Skip to content

Commit 583e9fe

Browse files
adparejondricek
andauthored
feat: add version to excel file generation (#234)
Co-authored-by: Jared Ondricek <jondricek@mitre.org>
1 parent 15c1a5f commit 583e9fe

1 file changed

Lines changed: 93 additions & 8 deletions

File tree

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()

0 commit comments

Comments
 (0)