1- """Functions to convert ATT&CK STIX data to Excel, as well as entrypoint for attackToExcel_cli ."""
1+ """Functions to convert ATT&CK STIX data to Excel, as well as entrypoint for attack-to-excel ."""
22
3- import argparse
43import os
54import re
65import tempfile
109
1110import pandas as pd
1211import requests
12+ import typer
1313from loguru import logger
1414from stix2 import MemoryStore
15+ from typing_extensions import Annotated
1516
1617from mitreattack import release_info
1718
@@ -38,6 +39,11 @@ class DomainConfig:
3839}
3940ATTACK_DOMAINS = tuple (DOMAIN_CONFIGS )
4041VALID_STIX_VERSIONS = ("2.0" , "2.1" )
42+ app = typer .Typer (
43+ add_completion = False ,
44+ no_args_is_help = True ,
45+ help = "Download ATT&CK data from MITRE/CTI and convert it to excel spreadsheets." ,
46+ )
4147
4248
4349def normalize_attack_version (version : str ) -> str :
@@ -586,99 +592,135 @@ def export(
586592 write_excel (dataframes = dataframes , domain = domain , src = mem_store , version = version , output_dir = output_dir )
587593
588594
589- def main (argv = None ):
590- """Entrypoint for attackToExcel_cli."""
591- parser = argparse .ArgumentParser (
592- description = "Download ATT&CK data from MITRE/CTI and convert it to excel spreadsheets"
593- )
594- parser .add_argument (
595- "--all-domains" ,
596- action = "store_true" ,
597- help = "export Excel files for all ATT&CK domains from a local or downloaded release" ,
598- )
599- parser .add_argument (
600- "-domain" ,
601- type = str ,
602- choices = ATTACK_DOMAINS ,
603- default = "enterprise-attack" ,
604- help = "which domain of ATT&CK to convert" ,
605- )
606- parser .add_argument (
607- "--domains" ,
608- type = str ,
609- nargs = "+" ,
610- choices = ATTACK_DOMAINS ,
611- help = "which ATT&CK domains to convert in --all-domains mode" ,
612- )
613- parser .add_argument (
614- "-version" ,
615- type = str ,
616- help = "which version of ATT&CK to convert. If omitted, builds the latest version" ,
617- )
618- parser .add_argument (
619- "--stix-version" ,
620- type = str ,
621- choices = VALID_STIX_VERSIONS ,
622- default = "2.0" ,
623- help = "STIX release tree to use in --all-domains mode" ,
624- )
625- parser .add_argument (
626- "-output" ,
627- type = str ,
628- default = None ,
629- help = "output directory. If omitted writes to a subfolder of the current directory depending on "
630- "the domain and version" ,
631- )
632- parser .add_argument (
633- "-remote" ,
634- type = str ,
635- default = None ,
636- help = "remote url of an ATT&CK workbench server." ,
637- )
638- parser .add_argument (
639- "-stix-file" ,
640- type = str ,
641- default = None ,
642- help = "Path to a local STIX file containing ATT&CK data for a domain, by default None" ,
643- )
644- parser .add_argument (
645- "--stix-base-dir" ,
646- type = str ,
647- default = None ,
648- help = "directory containing release STIX files for --all-domains mode" ,
649- )
650- parser .add_argument (
651- "--versioned-output-dir" ,
652- action = "store_true" ,
653- help = "preserve domain-version output folders in --all-domains mode" ,
654- )
655- args = parser .parse_args (args = argv )
656-
657- if args .domains and not args .all_domains :
658- parser .error ("--domains can only be used with --all-domains" )
659-
660- if args .all_domains :
661- if args .remote or args .stix_file :
662- parser .error ("--all-domains cannot be combined with -remote or -stix-file" )
663-
664- export_release (
665- version = args .version ,
666- stix_version = args .stix_version ,
667- output_dir = args .output or "output" ,
668- stix_base_dir = args .stix_base_dir ,
669- domains = args .domains ,
670- versioned_output_dir = args .versioned_output_dir ,
671- )
672- return
595+ def _validate_cli_value (value : str , allowed_values : tuple [str , ...], label : str ) -> str :
596+ """Return a CLI value after validating it against an allowed set."""
597+ if value not in allowed_values :
598+ allowed_values_text = ", " .join (allowed_values )
599+ raise typer .BadParameter (f"Invalid { label } : { value } . Expected one of: { allowed_values_text } " )
600+ return value
601+
602+
603+ @app .command ("from-stix" )
604+ def from_stix_cli (
605+ domain : Annotated [
606+ str ,
607+ typer .Option (
608+ "--domain" ,
609+ help = "ATT&CK domain STIX bundle to convert." ,
610+ ),
611+ ] = "enterprise-attack" ,
612+ version : Annotated [
613+ Optional [str ],
614+ typer .Option (
615+ "--version" ,
616+ help = "Which version of ATT&CK to convert. If omitted, builds the latest version." ,
617+ ),
618+ ] = None ,
619+ output : Annotated [
620+ str ,
621+ typer .Option (
622+ "--output" ,
623+ help = (
624+ "Output directory. If omitted writes to a subfolder of the current directory depending on the domain "
625+ "and version."
626+ ),
627+ ),
628+ ] = "." ,
629+ remote : Annotated [
630+ Optional [str ],
631+ typer .Option (
632+ "--remote" ,
633+ help = "Remote URL of an ATT&CK Workbench server." ,
634+ ),
635+ ] = None ,
636+ stix_file : Annotated [
637+ Optional [str ],
638+ typer .Option (
639+ "--stix-file" ,
640+ help = "Path to a local STIX file containing ATT&CK data for a domain." ,
641+ ),
642+ ] = None ,
643+ ):
644+ """Convert one ATT&CK domain STIX bundle to Excel."""
645+ domain = _validate_cli_value (domain , ATTACK_DOMAINS , "ATT&CK domain" )
646+
647+ if remote and stix_file :
648+ raise typer .BadParameter ("--remote and --stix-file are mutually exclusive" )
673649
674650 export (
675- domain = args .domain ,
676- version = args .version ,
677- output_dir = args .output or "." ,
678- remote = args .remote ,
679- stix_file = args .stix_file ,
651+ domain = domain ,
652+ version = version ,
653+ output_dir = output ,
654+ remote = remote ,
655+ stix_file = stix_file ,
656+ )
657+
658+
659+ @app .command ("from-release" )
660+ def from_release_cli (
661+ version : Annotated [
662+ Optional [str ],
663+ typer .Option (
664+ "--version" ,
665+ help = "Which ATT&CK release version to convert. If omitted, builds the latest version." ,
666+ ),
667+ ] = None ,
668+ domains : Annotated [
669+ Optional [List [str ]],
670+ typer .Option (
671+ "--domains" ,
672+ help = "ATT&CK release domain to include. Can be specified multiple times." ,
673+ ),
674+ ] = None ,
675+ stix_version : Annotated [
676+ str ,
677+ typer .Option (
678+ "--stix-version" ,
679+ help = "STIX release tree to use." ,
680+ ),
681+ ] = "2.0" ,
682+ stix_base_dir : Annotated [
683+ Optional [str ],
684+ typer .Option (
685+ "--stix-base-dir" ,
686+ help = "Directory containing release STIX files." ,
687+ ),
688+ ] = None ,
689+ output : Annotated [
690+ str ,
691+ typer .Option (
692+ "--output" ,
693+ help = "Parent output directory." ,
694+ ),
695+ ] = "output" ,
696+ versioned_output_dir : Annotated [
697+ bool ,
698+ typer .Option (
699+ "--versioned-output-dir" ,
700+ help = "Preserve domain-version output folders." ,
701+ ),
702+ ] = False ,
703+ ):
704+ """Convert ATT&CK release domain bundles to Excel."""
705+ stix_version = _validate_cli_value (stix_version , VALID_STIX_VERSIONS , "STIX version" )
706+ selected_domains = [
707+ _validate_cli_value (selected_domain , ATTACK_DOMAINS , "ATT&CK domain" ) for selected_domain in domains or []
708+ ]
709+
710+ export_release (
711+ version = version ,
712+ stix_version = stix_version ,
713+ output_dir = output ,
714+ stix_base_dir = stix_base_dir ,
715+ domains = selected_domains or None ,
716+ versioned_output_dir = versioned_output_dir ,
680717 )
681718
682719
720+ def main (argv = None ):
721+ """Entrypoint for attack-to-excel."""
722+ app (args = argv , prog_name = "attack-to-excel" )
723+
724+
683725if __name__ == "__main__" :
684726 main ()
0 commit comments