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+
27from 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 } \n Did 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+
33118if __name__ == "__main__" :
34119 main ()
0 commit comments