summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorW. Kosior <koszko@koszko.org>2025-01-09 20:00:03 +0100
committerW. Kosior <koszko@koszko.org>2025-01-09 20:13:13 +0100
commitfa66fac68f6726d8d15b7dbd116a6b29821443e8 (patch)
tree662cd51163424c3a063307bf53ea78a8227bb4c5
parent87276929e0ec1464626143e3d5212464fda8d61c (diff)
downloadAGH-threat-intel-course-fa66fac68f6726d8d15b7dbd116a6b29821443e8.tar.gz
AGH-threat-intel-course-fa66fac68f6726d8d15b7dbd116a6b29821443e8.zip
generate a cool map of targeted countries
-rw-r--r--Makefile11
-rwxr-xr-xcountries_motives_sectors_tables.py113
2 files changed, 115 insertions, 9 deletions
diff --git a/Makefile b/Makefile
index 969f873..03e92ec 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,8 @@
# Make sure you have Pandoc, a LaTeX distribution, Poppler (version 22.09.0 was
# used — might be relevant because we scrape the output of its `pdftohtml'),
# Python as well as Python packages `BeautifulSoup4', `pyyaml' (YAML parser
-# library) and `requests' installed.
+# library), `requests' and `geopandas' (version 0.14.2 was used, 1.0 will not
+# work) installed.
PYTHON=python3
PANDOC=pandoc
@@ -66,9 +67,9 @@ country motive sector:
true
.PHONY: country motive sector
-countries_table.tex: countries_motives_sectors_tables.py country \
+countries_table.tex target_map.svg: countries_motives_sectors_tables.py country \
blackobird_scraped_profiles.yaml
- $(PYTHON) $^ > $@
+ $(PYTHON) $^ > countries_table.tex
sectors_table.tex: countries_motives_sectors_tables.py sector \
blackobird_scraped_profiles.yaml
@@ -92,7 +93,9 @@ th-proj-archive.tar.gz: Makefile profiles.yaml scrape_mitre_groups_info.py \
clean:
rm -rf tables.md techniques_table.tex countries_table.tex \
- sectors_table.tex th-proj-archive.tar.gz $(DEFAULT_TARGETS)
+ motives_table.tex sectors_table.tex sectors_table.tex \
+ empty_world_map.svg target_map.svg th-proj-archive.tar.gz \
+ $(DEFAULT_TARGETS)
$(LATEXMK) -C
.PHONY: clean
diff --git a/countries_motives_sectors_tables.py b/countries_motives_sectors_tables.py
index d90a57c..f5d8e51 100755
--- a/countries_motives_sectors_tables.py
+++ b/countries_motives_sectors_tables.py
@@ -21,7 +21,7 @@ type_keys = {
}
trait_label_makers = {
- "country": (lambda country:
+ "country": (lambda country:
{
"usa": "USA",
"uk": "UK",
@@ -69,6 +69,96 @@ def read_APT_data(yaml_path):
else:
return yaml.safe_load(sys.stdin)
+gpd_country_names = {
+ "USA": "United States of America",
+ "UK": "United Kingdom",
+ "UAE": "United Arab Emirates",
+ "Southafrica": "South Africa",
+ "Newzealand": "New Zealand"
+}
+
+class Point:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+other_coords = {
+ "Hong Kong": Point(114., 22.4),
+ "Bahrain": Point(50.1, 26.2),
+ "Singapore": Point(103.77, 1.21)
+}
+
+chart_colors = ["#af0000", "#dae84d", "#009f00", "#0000af"]
+
+def plot_map(countries, targeting_percentages, dst_path="dst.svg"):
+ import geopandas as gpd
+ import matplotlib.pyplot as plt
+ from matplotlib.colors import ListedColormap
+ import pandas as pd
+ import math
+ from pathlib import Path
+ import os
+
+ world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+ world.plot(column='name', cmap=ListedColormap([[0.7, 0.7, 0.7]]))
+
+ plt.axis("off")
+ plt.margins(x=0, y=0)
+ plt.savefig("empty_world_map.svg", bbox_inches='tight', pad_inches=0)
+ plt.close()
+
+ os.system("head -n -1 empty_world_map.svg > target_map.svg")
+
+ centroids = world.centroid
+ centroid_list = pd.concat([world.name, centroids], axis=1)
+ map_width = 357.12
+ map_height = 172.521191
+ paths_markup = []
+ for country, percentages in zip(countries, targeting_percentages):
+ country_label = trait_label_makers["country"](country)
+ country_key = gpd_country_names.get(country_label, country_label)
+ mask = centroid_list["name"] == country_key
+ centroid_singleton_list = centroid_list[mask][0]
+ if (len(centroid_singleton_list) == 1):
+ point, = centroid_singleton_list
+ else:
+ point = other_coords.get(country_label, None)
+ if not point:
+ print(f"bad: {country_label}", file=sys.stderr)
+ continue
+
+ x = (point.x + 180) / 360 * map_width
+ y = (-point.y + 90) / 180 * map_height
+ # fix some vertical shift that occures for an unknown reason
+ y -= 3.7
+ radius = 4
+ angle = math.pi * 3 / 2
+ ratio = 2.
+
+ for percentage, color in sorted(zip(percentages, chart_colors),
+ key=lambda tup: tup[0],
+ reverse=True):
+ start_pos = [x+radius*math.cos(angle),
+ y+radius*math.sin(angle)]
+ start_pos = " ".join(str(i) for i in start_pos)
+ angle += percentage / 100 * math.pi / 2 * ratio
+ end_pos = [x+radius*math.cos(angle),
+ y+radius*math.sin(angle)]
+ end_pos = " ".join(str(i) for i in end_pos)
+ paths_markup.append(f"""\
+<path d="M{x} {y} {start_pos} A{radius} {radius} 0 0 1 {end_pos}Z"
+ fill="{color}"/>
+""")
+ with open("target_map.svg", "a") as target_map:
+ target_map.write("".join(paths_markup))
+ target_map.write("""
+ <defs>
+ <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}
+ #PatchCollection_1 > path {stroke: #fff!important; stroke-width: 0.3!important;}
+ </style>
+ </defs>
+</svg>""")
+
table_type = sys.argv[1] # "country", "sector" or "motive"
groups_data = read_APT_data(None if len(sys.argv) < 3 else sys.argv[2])
@@ -78,18 +168,25 @@ groups_by_trait_by_origin = {}
# Hand-picked groups that appear not to be state-sponsored.
ignored_groups = [
+ # We do not omit Wicked Spider because even tho it is not tagged as
+ # state-sponsored in the PDF is seems to have ties with Chinese authorities.
"Buhtrap", "Corkow", "FIN7", "Lurk", "MoneyTaker", "RTM", "Lunar Spider",
"Rocke", "Wizard Spider", "TA505", "DoppelSpider", "Dungeon Spider",
"GuruSpider", "Indrik Spider", "MontySpider", "OperationWindigo",
"PachaGroup", "PinchySpider", "Rocke", "SaltySpider", "Yingmob",
"ZombieSpider", "Avalanche", "Boss Spider", "CobaltGroup",
- "Cron", "GCMAN", "RetefeGang", "SharkSpider", "VenomSpider"
+ "Cron", "GCMAN", "RetefeGang", "SharkSpider", "VenomSpider",
+ # We also omit APT5 because neither the NSA'a Threat Hunting Guidance nor
+ # the description from
+ # https://web.archive.org/web/20180806122230/https://www.fireeye.com/current-threats/apt-groups.html
+ # actually state its origin.
+ "APT5"
]
for group in groups_data["groups"]:
if group["origin"] not in origin_labels or group["name"] in ignored_groups:
continue
-
+
origin = group["origin"]
groups_by_origin[origin] = groups_by_origin.get(origin, []) + [group]
@@ -129,17 +226,23 @@ p{0.65in} p{0.65in} p{0.65in} p{0.65in} \
print(f"{table_type} & {' & '.join(all_origin_labels)} & total APT count \\\\")
print("\\hline\\hline \\endhead")
+all_percentages = []
+
for trait in all_traits:
label = trait_label_makers[table_type](trait)
group_count = sum([len(groups_by_trait_by_origin[origin].get(trait, []))
for origin in all_origins])
+ percentages = [trait_percent(trait, origin) for origin in all_origins]
+ all_percentages.append(percentages)
group_precents_markup = ' & '.join(
- f"{round(trait_percent(trait, origin))}\\% groups"
- for origin in all_origins
+ f"{round(percent)}\\% groups" for percent in percentages
)
print(f"{label} & {group_precents_markup} & {group_count} \\\\")
print("\\end{longtable}")
print("}")
+
+if table_type == "country":
+ plot_map(all_traits, all_percentages)