Skip to content

Commit d0f0812

Browse files
committed
chore: merge with master
2 parents 4282a04 + c93b50f commit d0f0812

28 files changed

Lines changed: 4860 additions & 2216 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 pytest --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.

.readthedocs.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ build:
1212
- pip install uv
1313
- uv sync --extra docs
1414
build:
15-
- uv run sphinx-build -b html docs $READTHEDOCS_OUTPUT/html
15+
html:
16+
- uv run sphinx-build -b html docs $READTHEDOCS_OUTPUT/html
1617

1718
sphinx:
1819
configuration: docs/conf.py

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
- Before committing, run `just lint`.
1717
- `just lint`: run pre-commit hooks across the repo.
1818
- `just test`: run the pytest suite.
19+
- `just test-xdist`: run the pytest suite in parallel.
1920
- `just test-cov`: run tests with coverage for `mitreattack`.
21+
- `just test-cov-xdist`: run tests with coverage in parallel.
2022
- `just build`: build distributions with `uv build`.
2123
- Without `just`, run the same tools through `uv run ...`.
2224

@@ -32,6 +34,10 @@
3234
- Framework: `pytest` (with `pytest-cov` for coverage checks).
3335
- Place tests under `tests/` and name files/functions `test_*.py` / `test_*`.
3436
- Add or update tests for behavior changes, especially around STIX parsing and changelog/diff output paths.
37+
- Tests that need real ATT&CK STIX data should use the shared STIX fixtures instead of downloading or
38+
preparing bundles directly.
39+
- Parallel runs warm the shared STIX cache before workers start; update `DEFAULT_ATTACK_STIX_PREP` in
40+
`tests/conftest.py` if a new xdist-backed test needs another ATT&CK release.
3541
- Run `just test` locally before opening a PR; use `just test-cov` for larger changes.
3642

3743
## Commit & Pull Request Guidelines

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# mitreattack-python
22

3+
[![PyPI version](https://img.shields.io/pypi/v/mitreattack-python.svg)](https://pypi.org/project/mitreattack-python/) [![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/) [![License](https://img.shields.io/pypi/l/mitreattack-python.svg)](https://github.com/mitre-attack/mitreattack-python/blob/main/LICENSE) [![Docs](https://img.shields.io/readthedocs/mitreattack-python.svg)](https://mitreattack-python.readthedocs.io/) [![Lint and Test](https://img.shields.io/github/actions/workflow/status/mitre-attack/mitreattack-python/lint-and-test.yml?label=lint%20%26%20test)](https://github.com/mitre-attack/mitreattack-python/actions/workflows/lint-and-test.yml) [![Release and Publish](https://img.shields.io/github/actions/workflow/status/mitre-attack/mitreattack-python/release-and-publish.yml?branch=main&label=release)](https://github.com/mitre-attack/mitreattack-python/actions/workflows/release-and-publish.yml)
4+
35
This repository contains a library of Python tools and utilities for working with ATT&CK data.
46
For more information, see the [full documentation](https://mitreattack-python.readthedocs.io/) on ReadTheDocs.
57

conftest.py

Lines changed: 0 additions & 36 deletions
This file was deleted.

docs/CONTRIBUTING.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@ 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
56+
just test-xdist # Run tests in parallel
5557
just test-cov # Run tests with coverage report
58+
just test-cov-xdist # Run tests with coverage in parallel
5659
just build # Build the package
5760
```
5861

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+
64+
Tests that need real ATT&CK STIX data should use the shared STIX fixtures instead of downloading or
65+
preparing bundles directly. Parallel test runs warm the shared STIX cache before workers start; if a
66+
new xdist-backed test needs an additional ATT&CK release, update the cache warmup list in
67+
`tests/conftest.py`.
68+
5969
To run STIX-backed tests against specific local bundles, pass the bundle paths to pytest:
6070

6171
```bash

examples/generate_excel_files.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,65 @@
11
"""Generate ATT&CK Excel exports from local STIX bundles."""
22

33
import argparse
4-
import os
4+
from os import environ
5+
from pathlib import Path
56

67
from stix2 import MemoryStore
78

89
from mitreattack.attackToExcel import attackToExcel
910

1011
# Pass attack version via the command line or update the variable below
1112
DEFAULT_ATTACK_VERSION = "v19.0"
13+
# Parent directory where ATT&CK version export folders are written.
14+
OUTPUT_DIR = Path("output")
1215
# Set to true if you want the parent subfolder of the excel files to have a version.
1316
# Example - If you want the folder to be named enterprise-attack-v19.0 instead of enterprise-attack, set to True
1417
VERSIONED_OUTPUT_DIR = False
1518

1619

1720
def move_versioned_exports_to_domain_dir(output_dir, domain, version):
1821
"""Move versioned Excel exports into the unversioned domain folder."""
19-
versioned_dir = os.path.join(output_dir, f"{domain}-{version}")
20-
domain_dir = os.path.join(output_dir, domain)
22+
output_dir = Path(output_dir)
23+
versioned_dir = output_dir / f"{domain}-{version}"
24+
domain_dir = output_dir / domain
2125

22-
if not os.path.isdir(versioned_dir):
26+
if not versioned_dir.is_dir():
2327
return
2428

25-
os.makedirs(domain_dir, exist_ok=True)
29+
domain_dir.mkdir(parents=True, exist_ok=True)
2630

27-
for filename in os.listdir(versioned_dir):
28-
source_path = os.path.join(versioned_dir, filename)
29-
target_path = os.path.join(domain_dir, filename)
30-
31-
if not os.path.isfile(source_path):
31+
for source_path in versioned_dir.iterdir():
32+
if not source_path.is_file():
3233
continue
3334

34-
if os.path.exists(target_path):
35-
os.remove(target_path)
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
3656

37-
os.replace(source_path, target_path)
3857

39-
os.rmdir(versioned_dir)
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))
4063

4164

4265
def parse_args(argv=None):
@@ -49,10 +72,7 @@ def parse_args(argv=None):
4972
"-a",
5073
"--attack-version",
5174
default=DEFAULT_ATTACK_VERSION,
52-
help=(
53-
"ATT&CK version to export, such as v19.0. "
54-
f"Defaults to {DEFAULT_ATTACK_VERSION}."
55-
),
75+
help=(f"ATT&CK version to export, such as v19.0. Defaults to {DEFAULT_ATTACK_VERSION}."),
5676
)
5777
return parser.parse_args(args=argv)
5878

@@ -64,15 +84,16 @@ def main(argv=None):
6484

6585
# List of domains and version to process
6686
domains = ["enterprise-attack", "mobile-attack", "ics-attack"]
67-
output_dir = f"{attack_version}/"
87+
output_dir = OUTPUT_DIR / attack_version
6888

6989
# Path to the STIX bundles for each domain (assumes STIX files are downloaded)
70-
stix_base_dir = os.environ.get("STIX_BASE_DIR", f"attack-releases/stix-2.0/{attack_version}")
90+
stix_base_dir = Path(environ.get("STIX_BASE_DIR", Path("attack-releases") / "stix-2.0" / attack_version))
7191
stix_files = {
72-
"enterprise-attack": os.path.join(stix_base_dir, "enterprise-attack.json"),
73-
"mobile-attack": os.path.join(stix_base_dir, "mobile-attack.json"),
74-
"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",
7595
}
96+
validate_stix_files(stix_files, attack_version)
7697

7798
for domain in domains:
7899
stix_file = stix_files[domain]

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ default:
55
# Install development dependencies
66
install:
77
uv sync --all-extras
8+
uv run pre-commit install
9+
uv run pre-commit install --hook-type commit-msg
810

911
# Upgrade all uv dependencies
1012
upgrade:
@@ -35,6 +37,22 @@ ruff-format:
3537
test:
3638
uv run pytest
3739

40+
# Run the fast local test subset, excluding integration and slow tests
41+
test-fast:
42+
uv run pytest -m "not integration and not slow"
43+
44+
# Run the fast local test subset in parallel
45+
test-fast-xdist workers="auto":
46+
uv run --extra dev pytest -n {{ workers }} -m "not integration and not slow"
47+
48+
# Run tests in parallel
49+
test-xdist workers="auto":
50+
uv run --extra dev pytest -n {{ workers }}
51+
52+
# Run tests with coverage in parallel
53+
test-cov-xdist workers="auto":
54+
uv run --extra dev pytest -n {{ workers }} --cov=mitreattack
55+
3856
# Run tests with coverage
3957
test-cov:
4058
uv run pytest --cov=mitreattack

0 commit comments

Comments
 (0)