Skip to content

Commit 5e245fe

Browse files
authored
feat: attack v19 release (#232)
1 parent 4869e5f commit 5e245fe

11 files changed

Lines changed: 270 additions & 58 deletions

File tree

conftest.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Pytest command-line options for mitreattack-python."""
2+
3+
4+
def pytest_addoption(parser):
5+
"""Register pytest options for selecting ATT&CK STIX test data."""
6+
parser.addoption(
7+
"--stix-enterprise",
8+
action="store",
9+
default=None,
10+
help="Path to an Enterprise ATT&CK STIX bundle to use in tests.",
11+
)
12+
parser.addoption(
13+
"--stix-mobile",
14+
action="store",
15+
default=None,
16+
help="Path to a Mobile ATT&CK STIX bundle to use in tests.",
17+
)
18+
parser.addoption(
19+
"--stix-ics",
20+
action="store",
21+
default=None,
22+
help="Path to an ICS ATT&CK STIX bundle to use in tests.",
23+
)
24+
parser.addoption(
25+
"--attack-version",
26+
action="store",
27+
default=None,
28+
help="ATT&CK release version to download and use for STIX-backed tests.",
29+
)
30+
parser.addoption(
31+
"--stix-version",
32+
action="store",
33+
choices=("2.0", "2.1"),
34+
default="2.0",
35+
help="STIX version to download when --attack-version is used. Defaults to 2.0.",
36+
)

docs/CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ just test-cov # Run tests with coverage report
5656
just build # Build the package
5757
```
5858

59+
To run STIX-backed tests against specific local bundles, pass the bundle paths to pytest:
60+
61+
```bash
62+
uv run pytest \
63+
--stix-enterprise /path/to/enterprise-attack.json \
64+
--stix-mobile /path/to/mobile-attack.json \
65+
--stix-ics /path/to/ics-attack.json
66+
```
67+
68+
To have pytest download a specific ATT&CK release instead, use:
69+
70+
```bash
71+
uv run pytest --attack-version 16.1 --stix-version 2.1
72+
```
73+
5974
### Pull Requests
6075

6176
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/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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ package = true
7979
module-name = "mitreattack"
8080
module-root = ""
8181

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

tests/changelog/cli/test_argument_handling.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def _assert_default_args(self, args):
3838
assert args.verbose is False
3939
assert args.use_mitre_cti is False
4040
assert args.site_prefix == ""
41+
assert args.additional_formats_prefix == ""
4142

4243
def test_get_parsed_args_default_values(self, monkeypatch):
4344
"""Test default argument values."""
@@ -68,6 +69,8 @@ def test_get_parsed_args_all_options(self, monkeypatch):
6869
"layer3.json",
6970
"--site_prefix",
7071
"https://example.com",
72+
"--additional-formats-prefix",
73+
"/docs/changelogs/v16.1-v17.0",
7174
"--unchanged",
7275
"--show-key",
7376
"--no-contributors",
@@ -85,6 +88,7 @@ def test_get_parsed_args_all_options(self, monkeypatch):
8588
assert args.json_file == "test.json"
8689
assert args.layers == ["layer1.json", "layer2.json", "layer3.json"]
8790
assert args.site_prefix == "https://example.com"
91+
assert args.additional_formats_prefix == "/docs/changelogs/v16.1-v17.0"
8892
assert args.unchanged is True
8993
assert args.show_key is True
9094
assert args.contributors is False
@@ -182,6 +186,8 @@ def test_get_parsed_args_boolean_flags(self, flag, expected_attr, expected_value
182186
("--site_prefix", "https://custom.com", "site_prefix"),
183187
("--site_prefix", "", "site_prefix"), # Empty site prefix
184188
("--site_prefix", "https://example.com/", "site_prefix"), # With trailing slash
189+
("--additional-formats-prefix", "/docs/changelogs/v16.1-v17.0", "additional_formats_prefix"),
190+
("--additional-formats-prefix", "", "additional_formats_prefix"),
185191
],
186192
)
187193
def test_get_parsed_args_string_options(self, option, value, expected_attr, monkeypatch):

0 commit comments

Comments
 (0)