Skip to content

Commit f4afb1c

Browse files
committed
fix: improve file handling and path management in Excel export script
1 parent 24dbb32 commit f4afb1c

1 file changed

Lines changed: 44 additions & 23 deletions

File tree

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]

0 commit comments

Comments
 (0)