Skip to content

Commit e075a12

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex-prepare-xdist-setup
2 parents 98f668f + 15c1a5f commit e075a12

12 files changed

Lines changed: 328 additions & 65 deletions

File tree

docs/CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ preparing bundles directly. Parallel test runs warm the shared STIX cache before
6363
new xdist-backed test needs an additional ATT&CK release, update the cache warmup list in
6464
`tests/conftest.py`.
6565

66+
To run STIX-backed tests against specific local bundles, pass the bundle paths to pytest:
67+
68+
```bash
69+
uv run pytest \
70+
--stix-enterprise /path/to/enterprise-attack.json \
71+
--stix-mobile /path/to/mobile-attack.json \
72+
--stix-ics /path/to/ics-attack.json
73+
```
74+
75+
To have pytest download a specific ATT&CK release instead, use:
76+
77+
```bash
78+
uv run pytest --attack-version 16.1 --stix-version 2.1
79+
```
80+
6681
### Pull Requests
6782

6883
When making a pull request, please make sure to:

examples/generate_multiple_attack_diffs.py

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,75 @@
1+
"""Generate ATT&CK changelog outputs for multiple release pairs."""
2+
3+
import argparse
4+
15
from mitreattack.diffStix.changelog_helper import get_new_changelog_md
26

7+
DOMAINS = ["enterprise-attack", "mobile-attack", "ics-attack"]
8+
VERSION_PAIRS = [
9+
("17.1", "18.0"),
10+
("18.0", "18.1"),
11+
]
12+
13+
14+
def get_release_output_folder(old_version: str, new_version: str) -> str:
15+
"""Return the output folder for a release comparison."""
16+
return f"output/v{old_version}-v{new_version}"
17+
18+
19+
def get_artifact_link_prefix(old_version: str, new_version: str, *, attack_website_links: bool = False) -> str:
20+
"""Return the link prefix for generated layers and changelog JSON."""
21+
if not attack_website_links:
22+
return ""
23+
return f"/docs/changelogs/v{old_version}-v{new_version}"
24+
25+
26+
def get_parsed_args():
27+
"""Parse command line arguments for the example script."""
28+
parser = argparse.ArgumentParser(description="Generate ATT&CK changelog outputs for multiple release pairs.")
29+
parser.add_argument(
30+
"-w",
31+
"--attack-website-links",
32+
action="store_true",
33+
help="Use ATT&CK website paths for links to generated layers and changelog JSON.",
34+
)
35+
return parser.parse_args()
36+
37+
38+
def generate_diff(old_version: str, new_version: str, *, attack_website_links: bool = False):
39+
"""Generate changelog outputs for a single ATT&CK release pair."""
40+
output_folder = get_release_output_folder(old_version, new_version)
41+
print(f"Generating ATT&CK Diffs between {old_version}-{new_version}: {output_folder}")
42+
43+
get_new_changelog_md(
44+
domains=DOMAINS,
45+
layers=[
46+
f"{output_folder}/layer-enterprise.json",
47+
f"{output_folder}/layer-mobile.json",
48+
f"{output_folder}/layer-ics.json",
49+
],
50+
old=f"attack-releases/stix-2.0/v{old_version}",
51+
new=f"attack-releases/stix-2.0/v{new_version}",
52+
show_key=True,
53+
# site_prefix: str = "",
54+
verbose=True,
55+
include_contributors=True,
56+
markdown_file=f"{output_folder}/changelog.md",
57+
html_file=f"{output_folder}/index.html",
58+
html_file_detailed=f"{output_folder}/changelog-detailed.html",
59+
additional_formats_prefix=get_artifact_link_prefix(
60+
old_version,
61+
new_version,
62+
attack_website_links=attack_website_links,
63+
),
64+
json_file=f"{output_folder}/changelog.json",
65+
)
66+
367

468
def main():
5-
version_pairs = [
6-
("17.1", "18.0"),
7-
("18.0", "18.1"),
8-
]
9-
for version_pair in version_pairs:
10-
old_version = version_pair[0]
11-
new_version = version_pair[1]
12-
13-
output_folder = f"output/v{old_version}-v{new_version}"
14-
print(f"Generating ATT&CK Diffs between {old_version}-{new_version}: {output_folder}")
15-
16-
get_new_changelog_md(
17-
domains=["enterprise-attack", "mobile-attack", "ics-attack"],
18-
layers=[
19-
f"{output_folder}/layer-enterprise.json",
20-
f"{output_folder}/layer-mobile.json",
21-
f"{output_folder}/layer-ics.json",
22-
],
23-
old=f"attack-releases/stix-2.0/v{old_version}",
24-
new=f"attack-releases/stix-2.0/v{new_version}",
25-
show_key=True,
26-
# site_prefix: str = "",
27-
verbose=True,
28-
include_contributors=True,
29-
markdown_file=f"{output_folder}/changelog.md",
30-
html_file=f"{output_folder}/index.html",
31-
html_file_detailed=f"{output_folder}/changelog-detailed.html",
32-
json_file=f"{output_folder}/changelog.json",
33-
)
69+
"""Generate changelog outputs for all configured ATT&CK release pairs."""
70+
args = get_parsed_args()
71+
for old_version, new_version in VERSION_PAIRS:
72+
generate_diff(old_version, new_version, attack_website_links=args.attack_website_links)
3473

3574

3675
if __name__ == "__main__":

mitreattack/attackToExcel/stixToDf.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,9 +753,40 @@ def assetsToDf(src):
753753
related_assets_descriptions = []
754754

755755
for related_asset in asset["x_mitre_related_assets"]:
756-
related_assets.append(related_asset["name"])
757-
related_assets_sectors.append(", ".join(related_asset["related_asset_sectors"]))
758-
related_assets_descriptions.append(related_asset["description"])
756+
asset_name = row.get("name", "<unnamed asset>")
757+
asset_id = row.get("ID", "<missing ATT&CK ID>")
758+
asset_stix_id = row.get("STIX ID", "<missing STIX ID>")
759+
related_asset_name = related_asset.get("name", "<unnamed related asset>")
760+
761+
if "name" in related_asset:
762+
related_assets.append(related_asset["name"])
763+
else:
764+
logger.error(
765+
f"Missing name for related asset '{related_asset_name}' on asset '{asset_name}' "
766+
f"({asset_id}, {asset_stix_id}). Leaving related assets blank for this related asset "
767+
"and continuing export."
768+
)
769+
related_assets.append("")
770+
771+
if "related_asset_sectors" in related_asset:
772+
related_assets_sectors.append(", ".join(related_asset["related_asset_sectors"]))
773+
else:
774+
logger.error(
775+
f"Missing related_asset_sectors for related asset '{related_asset_name}' on asset "
776+
f"'{asset_name}' ({asset_id}, {asset_stix_id}). Leaving related assets sectors blank "
777+
"for this related asset and continuing export."
778+
)
779+
related_assets_sectors.append("")
780+
781+
if "description" in related_asset:
782+
related_assets_descriptions.append(related_asset["description"])
783+
else:
784+
logger.error(
785+
f"Missing description for related asset '{related_asset_name}' on asset '{asset_name}' "
786+
f"({asset_id}, {asset_stix_id}). Leaving related assets description blank for this "
787+
"related asset and continuing export."
788+
)
789+
related_assets_descriptions.append("")
759790

760791
row["related assets"] = "; ".join(related_assets)
761792
row["related assets sectors"] = "; ".join(related_assets_sectors)

mitreattack/diffStix/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Print full usage instructions:
1414
# You must run `pip install mitreattack-python` in order to access the diff_stix command
1515
diff_stix --help
1616
usage: diff_stix [-h] [--old OLD] [--new NEW] [--domains {enterprise-attack,mobile-attack,ics-attack} [{enterprise-attack,mobile-attack,ics-attack} ...]] [--markdown-file MARKDOWN_FILE] [--html-file HTML_FILE] [--html-file-detailed HTML_FILE_DETAILED]
17-
[--json-file JSON_FILE] [--layers [LAYERS ...]] [--site_prefix SITE_PREFIX] [--unchanged] [--use-mitre-cti] [--show-key] [--contributors] [--no-contributors] [-v]
17+
[--json-file JSON_FILE] [--layers [LAYERS ...]] [--site_prefix SITE_PREFIX] [--additional-formats-prefix ADDITIONAL_FORMATS_PREFIX] [--unchanged] [--use-mitre-cti] [--show-key] [--contributors] [--no-contributors] [-v]
1818

1919
Create changelog reports on the differences between two versions of the ATT&CK content. Takes STIX bundles as input. For default operation, put enterprise-attack.json, mobile-attack.json, and ics-attack.json bundles in 'old' and 'new' folders for the script to compare.
2020

@@ -37,6 +37,8 @@ options:
3737
output/January_2023_Updates_Mobile.json, output/January_2023_Updates_ICS.json, output/January_2023_Updates_Pre.json
3838
--site_prefix SITE_PREFIX
3939
Prefix links in markdown output, e.g. [prefix]/techniques/T1484
40+
--additional-formats-prefix ADDITIONAL_FORMATS_PREFIX
41+
Prefix detailed HTML links to generated layers and changelog JSON.
4042
--unchanged Show objects without changes in the markdown output
4143
--use-mitre-cti Use content from the MITRE CTI repo for the -old data
4244
--show-key Add a key explaining the change types to the markdown

mitreattack/diffStix/changelog_helper.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,7 +1871,14 @@ def layers_dict_to_files(outfiles, layers):
18711871
json.dump(layers["ics-attack"], open(ics_attack_layer_file, "w"), indent=4)
18721872

18731873

1874-
def write_detailed_html(html_file_detailed: str, diffStix: DiffStix):
1874+
def _get_additional_format_href(filename: str, additional_formats_prefix: str = "") -> str:
1875+
"""Return a link to an additional changelog output file."""
1876+
if not additional_formats_prefix:
1877+
return filename
1878+
return f"{additional_formats_prefix.rstrip('/')}/{filename}"
1879+
1880+
1881+
def write_detailed_html(html_file_detailed: str, diffStix: DiffStix, additional_formats_prefix: str = ""):
18751882
"""Write a detailed HTML report of changes between ATT&CK versions.
18761883
18771884
Parameters
@@ -1880,6 +1887,8 @@ def write_detailed_html(html_file_detailed: str, diffStix: DiffStix):
18801887
File to write HTML for the detailed changelog.
18811888
diffStix : DiffStix
18821889
An instance of a DiffStix object.
1890+
additional_formats_prefix : str, optional
1891+
Prefix for links to generated layer and JSON files, by default "".
18831892
"""
18841893
old_version = diffStix.data["old"]["enterprise-attack"]["attack_release_version"]
18851894
new_version = diffStix.data["new"]["enterprise-attack"]["attack_release_version"]
@@ -1889,6 +1898,11 @@ def write_detailed_html(html_file_detailed: str, diffStix: DiffStix):
18891898
else:
18901899
header = f"<h1>ATT&CK Changes Between v{old_version} and new content</h1>"
18911900

1901+
enterprise_layer_href = _get_additional_format_href("layer-enterprise.json", additional_formats_prefix)
1902+
mobile_layer_href = _get_additional_format_href("layer-mobile.json", additional_formats_prefix)
1903+
ics_layer_href = _get_additional_format_href("layer-ics.json", additional_formats_prefix)
1904+
changelog_json_href = _get_additional_format_href("changelog.json", additional_formats_prefix)
1905+
18921906
frontmatter = [
18931907
textwrap.dedent(
18941908
"""\
@@ -1913,7 +1927,7 @@ def write_detailed_html(html_file_detailed: str, diffStix: DiffStix):
19131927
header,
19141928
markdown.markdown(diffStix.get_md_key()),
19151929
textwrap.dedent(
1916-
"""\
1930+
f"""\
19171931
<table class=diff summary=Legends>
19181932
<tr>
19191933
<td>
@@ -1929,11 +1943,11 @@ def write_detailed_html(html_file_detailed: str, diffStix: DiffStix):
19291943
<h2>Additional formats</h2>
19301944
<p>These ATT&CK Navigator layer files can be uploaded to ATT&CK Navigator manually.</p>
19311945
<ul>
1932-
<li><a href="layer-enterprise.json">Enterprise changes</a></li>
1933-
<li><a href="layer-mobile.json">Mobile changes</a></li>
1934-
<li><a href="layer-ics.json">ICS changes</a></li>
1946+
<li><a href="{enterprise_layer_href}">Enterprise changes</a></li>
1947+
<li><a href="{mobile_layer_href}">Mobile changes</a></li>
1948+
<li><a href="{ics_layer_href}">ICS changes</a></li>
19351949
</ul>
1936-
<p>This JSON file contains the machine readble output used to create this page: <a href="changelog.json">changelog.json</a></p>
1950+
<p>This JSON file contains the machine readble output used to create this page: <a href="{changelog_json_href}">changelog.json</a></p>
19371951
"""
19381952
),
19391953
]
@@ -2256,6 +2270,13 @@ def get_parsed_args():
22562270
help="Prefix links in markdown output, e.g. [prefix]/techniques/T1484",
22572271
)
22582272

2273+
parser.add_argument(
2274+
"--additional-formats-prefix",
2275+
type=str,
2276+
default="",
2277+
help="Prefix detailed HTML links to generated layers and changelog JSON.",
2278+
)
2279+
22592280
parser.add_argument(
22602281
"--unchanged",
22612282
action="store_true",
@@ -2331,6 +2352,7 @@ def get_new_changelog_md(
23312352
markdown_file: Optional[str] = None,
23322353
html_file: Optional[str] = None,
23332354
html_file_detailed: Optional[str] = None,
2355+
additional_formats_prefix: str = "",
23342356
json_file: Optional[str] = None,
23352357
) -> str:
23362358
"""Get a Markdown string representation of differences between two ATT&CK versions.
@@ -2365,6 +2387,8 @@ def get_new_changelog_md(
23652387
If set, writes an HTML file from the parsed markdown, by default None
23662388
html_file_detailed : str, optional
23672389
If set, writes a more detailed HTML page, by default None
2390+
additional_formats_prefix : str, optional
2391+
Prefix for detailed HTML links to generated layer and JSON files, by default "".
23682392
json_file : str, optional
23692393
If set, writes JSON file of the changes, by default None
23702394
@@ -2414,7 +2438,11 @@ def get_new_changelog_md(
24142438
if html_file_detailed:
24152439
Path(html_file_detailed).parent.mkdir(parents=True, exist_ok=True)
24162440
logger.info("Writing detailed updates to file")
2417-
write_detailed_html(html_file_detailed=html_file_detailed, diffStix=diffStix)
2441+
write_detailed_html(
2442+
html_file_detailed=html_file_detailed,
2443+
diffStix=diffStix,
2444+
additional_formats_prefix=additional_formats_prefix,
2445+
)
24182446

24192447
if layers:
24202448
if len(layers) == 0:
@@ -2457,6 +2485,7 @@ def main():
24572485
markdown_file=args.markdown_file,
24582486
html_file=args.html_file,
24592487
html_file_detailed=args.html_file_detailed,
2488+
additional_formats_prefix=args.additional_formats_prefix,
24602489
json_file=args.json_file,
24612490
)
24622491

mitreattack/release_info.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# This file contains SHA256 hashes for officially released ATT&CK versions
99
# download_string = f"https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v{release}/{domain}-attack/{domain}-attack.json"
1010

11-
LATEST_VERSION = "18.1"
11+
LATEST_VERSION = "19.0"
1212

1313
STIX20 = {
1414
"enterprise": {
@@ -48,6 +48,7 @@
4848
"17.1": "9537a22166367a5b3c1434f5b17b27361cb9c88b34926e655344768fdbda3e85",
4949
"18.0": "6ecc9655954a4a0eeada8ba6f18f1d053fbcacce0e5411f677729d1dafce5673",
5050
"18.1": "628c4fc3c01b9ef37e1cd84ca3c421e1d43950a43464a14aabd1a7089601dc45",
51+
"19.0": "987d0cfddb1e65797457cf2b045df9161140ede71fd627f15dfdd73c2a2c72ab",
5152
},
5253
"mobile": {
5354
"3.0": "1385d94348054c1c1f7cdc652f0719db353b60c923949b10cbf8a2e815a86eb3",
@@ -89,6 +90,7 @@
8990
"17.1": "736078773f05ee943c0aa71bf71b935b04315c134809e8b678bd45c89cb1ab49",
9091
"18.0": "18ab338f8663200bdc62b982d5821ec255d9b420947b68e6d024000a44620404",
9192
"18.1": "62ecc7e3cabacc2430de0d65078a3726b6d8f7b0eff9493fd6403b514f66518d",
93+
"19.0": "1f3f050d6d4cb0a75a07a0c0dd0834ce2de176a0096a3ddd7ffc2ee2feeff592",
9294
},
9395
"ics": {
9496
"8.0": "2e9e9d0d9f0e5d14f64cf2788f46a1a4403bc88ab6ddd419cfcdfe617b0c920d",
@@ -115,6 +117,7 @@
115117
"17.1": "f0bd44fa2e167f2e9e94700f9081571dfedc49bebd856ea0d7ec24cf896d298b",
116118
"18.0": "e19597196d96ef07e7d1b0dc3a1e67f792a27f61d615a3242c694169fe81011c",
117119
"18.1": "76655cd7c363ca9a7474a95e9d60522a0c3211eaf2efad5b5e9cd7f9e0365b51",
120+
"19.0": "17f519e62fa85aded4ef6035dff348dd790dd177672757ceaded8e85f8caa950",
118121
},
119122
"pre": {
120123
"3.0": "bc59c1b1398a133cf0adb98e4e28396fdb6a5a2e2353cecb1783c425f066fc94",
@@ -172,6 +175,7 @@
172175
"17.1": "0d1c347a4d584cf7e11ef46556c33b7689341443bf86299188d46c307274323b",
173176
"18.0": "ff94838b09edfe7d59eec1cd7af0a1e229c4bc0ae0bdfa98ad170aeec9c3e272",
174177
"18.1": "f857d8f78f2f0c0b7db321a711a39fba98546c1e3076a657684850c83d0962fb",
178+
"19.0": "df520ea0775a57db7bff760145b02fed89290802913e056b7ed5970b02f3626a",
175179
},
176180
"mobile": {
177181
"1.0": "7da1903596bb69ef75a3c2a6c79e80328657bfed9226b2ed400ca18c88e0c1ea",
@@ -212,6 +216,7 @@
212216
"17.1": "33968697b94a5ff5568016a28bbcc93f7869dc2f2b2653ead833758867ab5bc9",
213217
"18.0": "f5f7f21c8daa59cc83f94432f0d77743be14d717f61f0464465b663508ef6d4f",
214218
"18.1": "c6dd56996586b2d1484e6555f9f5307f379dff24e7632e6af23097ef25656ea9",
219+
"19.0": "e6c65d1c5b22ad9eb52811c9a0b66f31537d4a4ea71f40da9dfee96412a42315",
215220
},
216221
"ics": {
217222
"8.0": "f3b53ff8d7f0f21f3e48c651edf68353aeb3e07727c32c3e47ef882e3bca10ab",
@@ -238,6 +243,7 @@
238243
"17.1": "cb207f963ca270994d9dabefe52237d46cf25056f154057f4b961f1c0803a8f3",
239244
"18.0": "e0c64def90415d548131009ba2ba4d8a4a725ca2293861a4cc2f9e8712625531",
240245
"18.1": "a7c0106492843485340710be9e841c1584d8fc6da8950e7097db0e7b5bc9f164",
246+
"19.0": "4a986f4a440aa0c36bd352e9b320de82160c456a680f7700fe4585d90b4a2522",
241247
},
242248
}
243249

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "mitreattack-python"
33
description = "MITRE ATT&CK python library"
4-
version = "5.4.6"
4+
version = "5.5.0"
55
authors = [{ name = "MITRE ATT&CK", email = "attack@mitre.org" }]
66
license = { text = "Apache-2.0" }
77
readme = "README.md"
@@ -80,6 +80,9 @@ package = true
8080
module-name = "mitreattack"
8181
module-root = ""
8282

83+
[tool.pytest.ini_options]
84+
testpaths = ["tests"]
85+
8386
[tool.ruff]
8487
line-length = 120
8588
extend-exclude = ["tests/resources/", "examples/", "__init__.py"]

0 commit comments

Comments
 (0)