11"""Generate ATT&CK Excel exports from local STIX bundles."""
22
33import argparse
4- import os
4+ from os import environ
5+ from pathlib import Path
56
67from stix2 import MemoryStore
78
89from mitreattack .attackToExcel import attackToExcel
910
1011# Pass attack version via the command line or update the variable below
1112DEFAULT_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
1417VERSIONED_OUTPUT_DIR = False
1518
1619
1720def 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 } \n Did 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
4265def 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 ]
0 commit comments