Skip to content

Commit f035eac

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex-prepare-xdist-setup
# Conflicts: # .github/workflows/lint-and-test.yml # docs/CONTRIBUTING.md # justfile # tests/changelog/conftest.py
2 parents e075a12 + c180361 commit f035eac

22 files changed

Lines changed: 3557 additions & 2107 deletions

.github/workflows/lint-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ jobs:
4747
run: uv sync --all-extras
4848

4949
- name: Run pytest
50-
run: uv run --extra dev pytest -n 2 --cov=mitreattack
50+
run: uv run --extra dev pytest -n 2 --cov=mitreattack --durations=20

.github/workflows/release-and-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
id: app-token
5757
uses: actions/create-github-app-token@v3
5858
with:
59-
app-id: ${{ vars.ATTACK_AUTOBOT_APP_ID }}
59+
client-id: ${{ vars.ATTACK_AUTOBOT_CLIENT_ID }}
6060
private-key: ${{ secrets.ATTACK_AUTOBOT_PRIVATE_KEY }}
6161

6262
# Note: We checkout the repository at the branch that triggered the workflow.

docs/CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,16 @@ Run `just` with no arguments to see all available commands. Here are the most co
5151

5252
```bash
5353
just lint # Run pre-commit hooks (ruff format) on all files
54-
just test # Run tests
54+
just test # Run the full test suite, matching CI expectations
55+
just test-fast # Run the fast local subset, excluding integration and slow tests
5556
just test-xdist # Run tests in parallel
5657
just test-cov # Run tests with coverage report
5758
just test-cov-xdist # Run tests with coverage in parallel
5859
just build # Build the package
5960
```
6061

62+
Use `just test-fast` while iterating locally on changes that do not need full STIX-backed export or other slow integration coverage. Tests or setup steps that normally take longer than 10 seconds should be marked `slow`, so they are skipped by `just test-fast`. Before opening a PR, run `just test`; GitHub Actions also runs the full suite with coverage.
63+
6164
Tests that need real ATT&CK STIX data should use the shared STIX fixtures instead of downloading or
6265
preparing bundles directly. Parallel test runs warm the shared STIX cache before workers start; if a
6366
new xdist-backed test needs an additional ATT&CK release, update the cache warmup list in

examples/generate_excel_files.py

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,99 @@
1-
from mitreattack.attackToExcel import attackToExcel
1+
"""Generate ATT&CK Excel exports from local STIX bundles."""
2+
3+
import argparse
4+
from os import environ
5+
from pathlib import Path
6+
27
from stix2 import MemoryStore
3-
import os
48

5-
def main():
9+
from mitreattack.attackToExcel import attackToExcel
10+
11+
# Pass attack version via the command line or update the variable below
12+
DEFAULT_ATTACK_VERSION = "v19.0"
13+
# Parent directory where ATT&CK version export folders are written.
14+
OUTPUT_DIR = Path("output")
15+
# Set to true if you want the parent subfolder of the excel files to have a version.
16+
# Example - If you want the folder to be named enterprise-attack-v19.0 instead of enterprise-attack, set to True
17+
VERSIONED_OUTPUT_DIR = False
18+
19+
20+
def move_versioned_exports_to_domain_dir(output_dir, domain, version):
21+
"""Move versioned Excel exports into the unversioned domain folder."""
22+
output_dir = Path(output_dir)
23+
versioned_dir = output_dir / f"{domain}-{version}"
24+
domain_dir = output_dir / domain
25+
26+
if not versioned_dir.is_dir():
27+
return
28+
29+
domain_dir.mkdir(parents=True, exist_ok=True)
30+
31+
for source_path in versioned_dir.iterdir():
32+
if not source_path.is_file():
33+
continue
34+
35+
target_path = domain_dir / source_path.name
36+
if target_path.exists():
37+
target_path.unlink()
38+
39+
source_path.replace(target_path)
40+
41+
versioned_dir.rmdir()
42+
43+
44+
def format_missing_stix_bundle_error(stix_file, attack_version):
45+
"""Format a concise missing STIX bundle error."""
46+
message = (
47+
f"STIX bundle not found: {stix_file}\n"
48+
"Download the STIX bundles before running this script, or set STIX_BASE_DIR to the directory containing "
49+
"enterprise-attack.json, mobile-attack.json, and ics-attack.json."
50+
)
51+
52+
if attack_version and not attack_version.startswith("v"):
53+
message = f"{message}\nDid you mean -a v{attack_version}?"
54+
55+
return message
56+
57+
58+
def validate_stix_files(stix_files, attack_version):
59+
"""Exit with a clean error if any expected STIX bundle is missing."""
60+
for stix_file in stix_files.values():
61+
if not stix_file.is_file():
62+
raise SystemExit(format_missing_stix_bundle_error(stix_file, attack_version))
63+
64+
65+
def parse_args(argv=None):
66+
"""Parse command line arguments."""
67+
parser = argparse.ArgumentParser(
68+
prog="generate_excel_files.py",
69+
description="Generate ATT&CK Excel exports from local STIX bundles.",
70+
)
71+
parser.add_argument(
72+
"-a",
73+
"--attack-version",
74+
default=DEFAULT_ATTACK_VERSION,
75+
help=(f"ATT&CK version to export, such as v19.0. Defaults to {DEFAULT_ATTACK_VERSION}."),
76+
)
77+
return parser.parse_args(args=argv)
78+
79+
80+
def main(argv=None):
81+
"""Generate excel files for specific versions of ATT&CK."""
82+
args = parse_args(argv)
83+
attack_version = args.attack_version
84+
685
# List of domains and version to process
786
domains = ["enterprise-attack", "mobile-attack", "ics-attack"]
8-
output_dir = "output/"
87+
output_dir = OUTPUT_DIR / attack_version
988

1089
# Path to the STIX bundles for each domain (assumes STIX files are downloaded)
11-
stix_base_dir = os.environ.get("STIX_BASE_DIR", "attack-releases/stix-2.0/v18.0")
90+
stix_base_dir = Path(environ.get("STIX_BASE_DIR", Path("attack-releases") / "stix-2.0" / attack_version))
1291
stix_files = {
13-
"enterprise-attack": os.path.join(stix_base_dir, "enterprise-attack.json"),
14-
"mobile-attack": os.path.join(stix_base_dir, "mobile-attack.json"),
15-
"ics-attack": os.path.join(stix_base_dir, "ics-attack.json"),
92+
"enterprise-attack": stix_base_dir / "enterprise-attack.json",
93+
"mobile-attack": stix_base_dir / "mobile-attack.json",
94+
"ics-attack": stix_base_dir / "ics-attack.json",
1695
}
96+
validate_stix_files(stix_files, attack_version)
1797

1898
for domain in domains:
1999
stix_file = stix_files[domain]
@@ -26,9 +106,14 @@ def main():
26106
# Export to Excel
27107
attackToExcel.export(
28108
domain=domain,
109+
version=attack_version,
29110
output_dir=output_dir,
30111
mem_store=mem_store,
31112
)
32113

114+
if attack_version and not VERSIONED_OUTPUT_DIR:
115+
move_versioned_exports_to_domain_dir(output_dir=output_dir, domain=domain, version=attack_version)
116+
117+
33118
if __name__ == "__main__":
34119
main()

examples/generate_multiple_attack_diffs.py

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import argparse
44

5-
from mitreattack.diffStix.changelog_helper import get_new_changelog_md
5+
from mitreattack.diffStix.attack_changelog import generate_attack_changelog
66

77
DOMAINS = ["enterprise-attack", "mobile-attack", "ics-attack"]
88
VERSION_PAIRS = [
@@ -11,17 +11,6 @@
1111
]
1212

1313

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-
2514

2615
def get_parsed_args():
2716
"""Parse command line arguments for the example script."""
@@ -37,31 +26,18 @@ def get_parsed_args():
3726

3827
def generate_diff(old_version: str, new_version: str, *, attack_website_links: bool = False):
3928
"""Generate changelog outputs for a single ATT&CK release pair."""
40-
output_folder = get_release_output_folder(old_version, new_version)
29+
output_folder = f"output/v{old_version}-v{new_version}"
4130
print(f"Generating ATT&CK Diffs between {old_version}-{new_version}: {output_folder}")
4231

43-
get_new_changelog_md(
32+
generate_attack_changelog(
33+
old_version=old_version,
34+
new_version=new_version,
4435
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 = "",
36+
output_dir=output_folder,
5437
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",
38+
markdown_file=True,
39+
html_file=True,
40+
attack_website_links=attack_website_links,
6541
)
6642

6743

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ ruff-format:
3535
test:
3636
uv run pytest
3737

38+
# Run the fast local test subset, excluding integration and slow tests
39+
test-fast:
40+
uv run pytest -m "not integration and not slow"
41+
3842
# Run tests in parallel
3943
test-xdist workers="auto":
4044
uv run --extra dev pytest -n {{ workers }}

mitreattack/attackToExcel/README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,40 @@ It also provides a means to access ATT&CK data as [Pandas](https://pandas.pydata
1010
Print full usage instructions:
1111

1212
```shell
13-
python3 attackToExcel.py -h
13+
attack-to-excel --help
1414
```
1515

1616
Example execution:
1717

1818
```shell
19-
python3 attackToExcel.py
19+
attack-to-excel from-stix
2020
```
2121

2222
Build a excel files corresponding to a specific domain and version of ATT&CK:
2323

2424
```shell
25-
python3 attackToExcel -domain mobile-attack -version v5.0
25+
attack-to-excel from-stix --domain mobile-attack --version v5.0
26+
```
27+
28+
Build Excel files for all ATT&CK domains from a release. If local STIX files
29+
are missing under `attack-releases/stix-2.0/v19.0`, they are downloaded
30+
temporarily for the export:
31+
32+
```shell
33+
attack-to-excel from-release --version v19.0
34+
```
35+
36+
To persist release STIX files before exporting, use `download_attack_stix`:
37+
38+
```shell
39+
download_attack_stix -v 19.0
40+
attack-to-excel from-release --version v19.0
41+
```
42+
43+
Build Excel files for selected ATT&CK domains from a release:
44+
45+
```shell
46+
attack-to-excel from-release --version v19.0 --domains mobile-attack --domains ics-attack
2647
```
2748

2849
### Module
@@ -35,6 +56,14 @@ import mitreattack.attackToExcel.attackToExcel as attackToExcel
3556
attackToExcel.export("mobile-attack", "v5.0", "/path/to/export/folder")
3657
```
3758

59+
Example execution targeting all release domains:
60+
61+
```python
62+
import mitreattack.attackToExcel.attackToExcel as attackToExcel
63+
64+
attackToExcel.export_release(version="v19.0", output_dir="output")
65+
```
66+
3867
## Interfaces
3968

4069
### attackToExcel
@@ -48,6 +77,7 @@ overview of the available methods follows.
4877
|build_dataframes| `src`: MemoryStore or other stix2 DataSource object holding domain data<br> `domain`: domain of ATT&CK that `src` corresponds to| Builds a Pandas DataFrame collection as a dictionary, with keys for each type, based on the ATT&CK data provided|
4978
|write_excel| `dataframes`: pandas DataFrame dictionary (generated by build_dataframes) <br> `domain`: domain of ATT&CK that `dataframes` corresponds to <br> `version`: optional parameter indicating which version of ATT&CK is in use <br> `output_dir`: optional parameter specifying output directory| Writes out DataFrame based ATT&CK data to excel files|
5079
|export| `domain`: the domain of ATT&CK to download <br> `version`: optional parameter specifying which version of ATT&CK to download <br> `output_dir`: optional parameter specifying output directory| Downloads ATT&CK data from MITRE/CTI and exports it to Excel spreadsheets |
80+
|export_release| `version`: optional ATT&CK release version <br> `stix_version`: STIX release tree, such as "2.0" or "2.1" <br> `output_dir`: parent output directory <br> `stix_base_dir`: optional directory containing release STIX files <br> `domains`: optional list of domains <br> `versioned_output_dir`: preserve domain-version output folders| Exports a full ATT&CK release to Excel spreadsheets, downloading missing STIX files temporarily when needed |
5181

5282
### stixToDf
5383

0 commit comments

Comments
 (0)