diff options
author | W. Kosior <koszko@koszko.org> | 2025-01-09 20:00:03 +0100 |
---|---|---|
committer | W. Kosior <koszko@koszko.org> | 2025-01-09 20:13:13 +0100 |
commit | fa66fac68f6726d8d15b7dbd116a6b29821443e8 (patch) | |
tree | 662cd51163424c3a063307bf53ea78a8227bb4c5 | |
parent | 87276929e0ec1464626143e3d5212464fda8d61c (diff) | |
download | AGH-threat-intel-course-fa66fac68f6726d8d15b7dbd116a6b29821443e8.tar.gz AGH-threat-intel-course-fa66fac68f6726d8d15b7dbd116a6b29821443e8.zip |
generate a cool map of targeted countries
-rw-r--r-- | Makefile | 11 | ||||
-rwxr-xr-x | countries_motives_sectors_tables.py | 113 |
2 files changed, 115 insertions, 9 deletions
@@ -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) |