""" Atomic Red Team documentation generator. This module generates all documentation including: - Individual technique markdown files - ATT&CK matrices (markdown) - Platform-specific indexes (markdown, CSV, YAML) - ATT&CK Navigator layers (JSON) """ import csv import json import re from concurrent.futures import ProcessPoolExecutor, as_completed from io import StringIO from pathlib import Path from typing import Dict, List, Optional, Pattern, Tuple from atomic_red_team.attack_api import ATTACK_API from atomic_red_team.utils import ATOMIC_RED_TEAM, AtomicRedTeam import yaml # Platform configurations for index generation PLATFORM_CONFIGS = { "all": {"pattern": re.compile(r".*"), "attack_pattern": re.compile(r".*")}, "windows": { "pattern": re.compile(r"windows"), "attack_pattern": re.compile(r"windows"), }, "macos": { "pattern": re.compile(r"macos"), "attack_pattern": re.compile(r"windows"), }, "linux": { "pattern": re.compile(r"linux"), "attack_pattern": re.compile(r"windows"), }, "iaas": {"pattern": re.compile(r"iaas"), "attack_pattern": re.compile(r"windows")}, "containers": { "pattern": re.compile(r"containers"), "attack_pattern": re.compile(r"windows"), }, "office-365": { "pattern": re.compile(r"office-365"), "attack_pattern": re.compile(r"office"), }, "google-workspace": { "pattern": re.compile(r"google-workspace"), "attack_pattern": re.compile(r"office"), }, "azure-ad": { "pattern": re.compile(r"azure-ad"), "attack_pattern": re.compile(r"identity"), }, "esxi": {"pattern": re.compile(r"esxi"), "attack_pattern": re.compile(r"esxi")}, "iaas:gcp": { "pattern": re.compile(r"iaas:gcp"), "attack_pattern": re.compile(r".*"), }, "iaas:azure": { "pattern": re.compile(r"iaas:azure"), "attack_pattern": re.compile(r".*"), }, "iaas:aws": { "pattern": re.compile(r"iaas:aws"), "attack_pattern": re.compile(r".*"), }, } def _generate_technique_doc_worker( args: Tuple[dict, str], ) -> Tuple[str, bool, Optional[str]]: """Standalone function for ProcessPoolExecutor to generate a single technique doc.""" atomic_yaml, atomics_directory = args try: art = AtomicRedTeam(atomics_directory=atomics_directory) yaml_path = atomic_yaml["atomic_yaml_path"] md_path = yaml_path.replace(".yaml", ".md") technique_id = atomic_yaml.get("attack_technique", "").upper() art.generate_technique_docs(technique_id, md_path) return (yaml_path, True, None) except Exception as ex: return (atomic_yaml.get("atomic_yaml_path", "unknown"), False, str(ex)) def _generate_matrix_worker(args: Tuple[str, str, str, Optional[str]]) -> None: """Standalone function for ProcessPoolExecutor to generate a matrix.""" title_prefix, output_path, atomics_directory, platform_pattern = args import importlib from pathlib import Path doc_generator = importlib.import_module('atomic_red_team.doc_generator') utils = importlib.import_module('atomic_red_team.utils') art = utils.AtomicRedTeam(atomics_directory=atomics_directory) docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) pattern = re.compile(platform_pattern) if platform_pattern else re.compile(r".*") docs.generate_attack_matrix(title_prefix, Path(output_path), only_platform=pattern) def _generate_index_worker( args: Tuple[str, str, str, Optional[str], Optional[str]], ) -> None: """Standalone function for ProcessPoolExecutor to generate a markdown index.""" ( title_prefix, output_path, atomics_directory, only_platform_pattern, attack_platform_pattern, ) = args import importlib from pathlib import Path doc_generator = importlib.import_module('atomic_red_team.doc_generator') utils = importlib.import_module('atomic_red_team.utils') art = utils.AtomicRedTeam(atomics_directory=atomics_directory) docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) only_platform = ( re.compile(only_platform_pattern) if only_platform_pattern else re.compile(r".*") ) attack_platform = ( re.compile(attack_platform_pattern) if attack_platform_pattern else re.compile(r".*") ) docs.generate_index( title_prefix, Path(output_path), only_platform=only_platform, attack_platform=attack_platform, ) def _generate_index_csv_worker( args: Tuple[str, str, Optional[str], Optional[str]], ) -> None: """Standalone function for ProcessPoolExecutor to generate a CSV index.""" output_path, atomics_directory, only_platform_pattern, attack_platform_pattern = ( args ) import importlib from pathlib import Path doc_generator = importlib.import_module('atomic_red_team.doc_generator') utils = importlib.import_module('atomic_red_team.utils') art = utils.AtomicRedTeam(atomics_directory=atomics_directory) docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) only_platform = ( re.compile(only_platform_pattern) if only_platform_pattern else re.compile(r".*") ) attack_platform = ( re.compile(attack_platform_pattern) if attack_platform_pattern else re.compile(r".*") ) docs.generate_index_csv( Path(output_path), only_platform=only_platform, attack_platform=attack_platform ) def _generate_yaml_index_worker(args: Tuple[str, str]) -> None: """Standalone function for ProcessPoolExecutor to generate a YAML index.""" output_path, atomics_directory = args import importlib from pathlib import Path doc_generator = importlib.import_module('atomic_red_team.doc_generator') utils = importlib.import_module('atomic_red_team.utils') art = utils.AtomicRedTeam(atomics_directory=atomics_directory) docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) docs.generate_yaml_index(Path(output_path)) def _generate_yaml_index_by_platform_worker(args: Tuple[str, str, str]) -> None: """Standalone function for ProcessPoolExecutor to generate a platform-specific YAML index.""" output_path, atomics_directory, platform = args import importlib from pathlib import Path doc_generator = importlib.import_module('atomic_red_team.doc_generator') utils = importlib.import_module('atomic_red_team.utils') art = utils.AtomicRedTeam(atomics_directory=atomics_directory) docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) docs.generate_yaml_index_by_platform(Path(output_path), platform) class AtomicRedTeamDocs: """ Documentation generator for Atomic Red Team. Generates all documentation including technique docs, indexes, matrices, and ATT&CK Navigator layers. """ def __init__(self, atomic_red_team: Optional[AtomicRedTeam] = None): """Initialize the documentation generator.""" self.atomic_red_team = atomic_red_team or ATOMIC_RED_TEAM self.atomics_directory = self.atomic_red_team.atomics_directory def generate_all_the_docs(self) -> Tuple[List[str], List[str]]: """ Generate all documentation used by Atomic Red Team. Returns: Tuple of (successful_paths, failed_paths) """ oks = [] fails = [] # Generate individual technique docs concurrently with ProcessPoolExecutor() as executor: future_to_yaml = { executor.submit( _generate_technique_doc_worker, (atomic_yaml, self.atomics_directory), ): atomic_yaml for atomic_yaml in self.atomic_red_team.atomic_tests } for future in as_completed(future_to_yaml): yaml_path, success, error = future.result() if success: oks.append(yaml_path) else: fails.append(yaml_path) print(f"✗ {yaml_path}: {error}") print(f"\nGenerated docs for {len(oks)} techniques, {len(fails)} failures") # Prepare directories indexes_dir = Path(self.atomics_directory) / "Indexes" matrices_dir = indexes_dir / "Matrices" md_indexes_dir = indexes_dir / "Indexes-Markdown" csv_indexes_dir = indexes_dir / "Indexes-CSV" layers_dir = indexes_dir / "Attack-Navigator-Layers" for dir_path in [matrices_dir, md_indexes_dir, csv_indexes_dir, layers_dir]: dir_path.mkdir(parents=True, exist_ok=True) print("\nGenerating indexes and matrices concurrently...") # Prepare all index generation tasks tasks = [] # ATT&CK matrices tasks.append(("matrix", _generate_matrix_worker, ("All", str(matrices_dir / "matrix.md"), self.atomics_directory, None))) tasks.append(("windows-matrix", _generate_matrix_worker, ("Windows", str(matrices_dir / "windows-matrix.md"), self.atomics_directory, r"windows"))) tasks.append(("macos-matrix", _generate_matrix_worker, ("macOS", str(matrices_dir / "macos-matrix.md"), self.atomics_directory, r"macos"))) tasks.append(("linux-matrix", _generate_matrix_worker, ("Linux", str(matrices_dir / "linux-matrix.md"), self.atomics_directory, r"linux"))) tasks.append(("esxi-matrix", _generate_matrix_worker, ("ESXi", str(matrices_dir / "esxi-matrix.md"), self.atomics_directory, r"esxi"))) # Markdown indexes tasks.append(("md-index-all", _generate_index_worker, ("All", str(md_indexes_dir / "index.md"), self.atomics_directory, None, None))) tasks.append(("md-index-windows", _generate_index_worker, ("Windows", str(md_indexes_dir / "windows-index.md"), self.atomics_directory, r"windows", r"windows"))) tasks.append(("md-index-macos", _generate_index_worker, ("macOS", str(md_indexes_dir / "macos-index.md"), self.atomics_directory, r"macos", r"windows"))) tasks.append(("md-index-linux", _generate_index_worker, ("Linux", str(md_indexes_dir / "linux-index.md"), self.atomics_directory, r"linux", r"windows"))) tasks.append(("md-index-iaas", _generate_index_worker, ("IaaS", str(md_indexes_dir / "iaas-index.md"), self.atomics_directory, r"iaas", r"windows"))) tasks.append(("md-index-containers", _generate_index_worker, ("Containers", str(md_indexes_dir / "containers-index.md"), self.atomics_directory, r"containers", r"windows"))) tasks.append(("md-index-office365", _generate_index_worker, ("Office 365", str(md_indexes_dir / "office-365-index.md"), self.atomics_directory, r"office-365", r"office"))) tasks.append(("md-index-google-workspace", _generate_index_worker, ("Google Workspace", str(md_indexes_dir / "google-workspace-index.md"), self.atomics_directory, r"google-workspace", r"office"))) tasks.append(("md-index-azure-ad", _generate_index_worker, ("Azure AD", str(md_indexes_dir / "azure-ad-index.md"), self.atomics_directory, r"azure-ad", r"identity"))) tasks.append(("md-index-esxi", _generate_index_worker, ("ESXi", str(md_indexes_dir / "esxi-index.md"), self.atomics_directory, r"esxi", r"esxi"))) # CSV indexes tasks.append(("csv-index-all", _generate_index_csv_worker, (str(csv_indexes_dir / "index.csv"), self.atomics_directory, None, None))) tasks.append(("csv-index-windows", _generate_index_csv_worker, (str(csv_indexes_dir / "windows-index.csv"), self.atomics_directory, r"windows", r"windows"))) tasks.append(("csv-index-macos", _generate_index_csv_worker, (str(csv_indexes_dir / "macos-index.csv"), self.atomics_directory, r"macos", r"macos"))) tasks.append(("csv-index-linux", _generate_index_csv_worker, (str(csv_indexes_dir / "linux-index.csv"), self.atomics_directory, r"linux", r"linux"))) tasks.append(("csv-index-iaas", _generate_index_csv_worker, (str(csv_indexes_dir / "iaas-index.csv"), self.atomics_directory, r"iaas", r"iaas"))) tasks.append(("csv-index-containers", _generate_index_csv_worker, (str(csv_indexes_dir / "containers-index.csv"), self.atomics_directory, r"containers", r"containers"))) tasks.append(("csv-index-office365", _generate_index_csv_worker, (str(csv_indexes_dir / "office-365-index.csv"), self.atomics_directory, r"office-365", r"office"))) tasks.append(("csv-index-google-workspace", _generate_index_csv_worker, (str(csv_indexes_dir / "google-workspace-index.csv"), self.atomics_directory, r"google-workspace", r"identity"))) tasks.append(("csv-index-azure-ad", _generate_index_csv_worker, (str(csv_indexes_dir / "azure-ad-index.csv"), self.atomics_directory, r"azure-ad", r"identity"))) tasks.append(("csv-index-esxi", _generate_index_csv_worker, (str(csv_indexes_dir / "esxi-index.csv"), self.atomics_directory, r"esxi", r"esxi"))) # YAML indexes tasks.append(("yaml-index-all", _generate_yaml_index_worker, (str(indexes_dir / "index.yaml"), self.atomics_directory))) for platform in ["windows", "macos", "linux", "office-365", "azure-ad", "google-workspace", "iaas", "containers", "iaas:gcp", "iaas:azure", "iaas:aws", "esxi"]: filename = f"{platform.replace(':', '_')}-index.yaml" tasks.append((f"yaml-index-{platform}", _generate_yaml_index_by_platform_worker, (str(indexes_dir / filename), self.atomics_directory, platform))) # Generate all indexes concurrently with ProcessPoolExecutor() as executor: future_to_task = {executor.submit(task[1], task[2]): task[0] for task in tasks} for future in as_completed(future_to_task): task_name = future_to_task[future] try: future.result() except Exception as ex: print(f"✗ Error generating {task_name}: {ex}") # Generate ATT&CK Navigator layers (this is already optimized internally) print("\nGenerating ATT&CK Navigator layers...") self.generate_navigator_layers(layers_dir) return oks, fails def generate_attack_matrix( self, title_prefix: str, output_path: Path, only_platform: Pattern = re.compile(r".*"), ) -> None: """Generate a Markdown ATT&CK matrix.""" result = f"# {title_prefix} Atomic Tests by ATT&CK Tactic & Technique\n" result += f"| {' | '.join(ATTACK_API.ordered_tactics)} |\n" result += f"|{'-----|' * len(ATTACK_API.ordered_tactics)}\n" matrix = ATTACK_API.ordered_tactic_to_technique_matrix( only_platform=only_platform ) for row in matrix: row_values = [] for technique in row: if technique: row_values.append( self.atomic_red_team.github_link_to_technique( technique, include_identifier=False, only_platform=only_platform, ) ) else: row_values.append("") result += f"| {' | '.join(row_values)} |\n" output_path.write_text(result, encoding="utf-8") print(f"Generated ATT&CK matrix at {output_path}") def generate_index( self, title_prefix: str, output_path: Path, only_platform: Pattern = re.compile(r".*"), attack_platform: Pattern = re.compile(r".*"), ) -> None: """Generate a Markdown index of ATT&CK Tactic -> Technique -> Atomic Tests.""" result = f"# {title_prefix} Atomic Tests by ATT&CK Tactic & Technique\n" techniques_by_tactic = ATTACK_API.techniques_by_tactic( only_platform=attack_platform ) for tactic, techniques in techniques_by_tactic.items(): result += f"# {tactic}\n" for technique in techniques: result += f"- {self.atomic_red_team.github_link_to_technique(technique, include_identifier=True, only_platform=only_platform)}\n" atomic_tests = self.atomic_red_team.atomic_tests_for_technique( technique ) for i, atomic_test in enumerate(atomic_tests): platforms = atomic_test.get("supported_platforms", []) if any(only_platform.match(p.lower()) for p in platforms): result += f" - Atomic Test #{i + 1}: {atomic_test['name']} [{', '.join(platforms)}]\n" result += "\n" output_path.write_text(result, encoding="utf-8") print(f"Generated Atomic Red Team index at {output_path}") def generate_index_csv( self, output_path: Path, only_platform: Pattern = re.compile(r".*"), attack_platform: Pattern = re.compile(r".*"), ) -> None: """Generate a CSV index.""" output = StringIO(newline="") writer = csv.writer(output, lineterminator="\n") writer.writerow( [ "Tactic", "Technique #", "Technique Name", "Test #", "Test Name", "Test GUID", "Executor Name", ] ) techniques_by_tactic = ATTACK_API.techniques_by_tactic( only_platform=attack_platform ) for tactic, techniques in techniques_by_tactic.items(): for technique in techniques: tech_id = ATTACK_API.technique_identifier_for_technique(technique) # Get atomic YAML to use display_name (which has full technique name for sub-techniques) atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) if not atomic_yaml: continue tech_name = atomic_yaml.get("display_name", technique.get("name", "")) atomic_tests = self.atomic_red_team.atomic_tests_for_technique( technique ) for i, atomic_test in enumerate(atomic_tests): platforms = atomic_test.get("supported_platforms", []) if any(only_platform.match(p.lower()) for p in platforms): writer.writerow( [ tactic, tech_id, tech_name, i + 1, atomic_test.get("name", ""), atomic_test.get("auto_generated_guid", ""), atomic_test.get("executor", {}).get("name", ""), ] ) output_path.write_text(output.getvalue(), encoding="utf-8") print(f"Generated Atomic Red Team CSV index at {output_path}") def generate_yaml_index(self, output_path: Path) -> None: """Generate a master YAML index.""" result: Dict[str, dict] = {} techniques_by_tactic = ATTACK_API.techniques_by_tactic() for tactic, techniques in techniques_by_tactic.items(): result[tactic] = {} for technique in techniques: tech_id = ATTACK_API.technique_identifier_for_technique(technique) # Create a copy of the technique and update name with display_name from YAML technique_copy = json.loads(json.dumps(technique)) # Deep copy atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) if atomic_yaml and atomic_yaml.get("display_name"): technique_copy["name"] = atomic_yaml["display_name"] result[tactic][tech_id] = { "technique": technique_copy, "atomic_tests": self.atomic_red_team.atomic_tests_for_technique( technique ), } # Convert through JSON to eliminate YAML aliases (matching Ruby behavior) # Use explicit_start=True to add '---' at the beginning like Ruby yaml_content = yaml.dump( json.loads(json.dumps(result)), default_flow_style=False, allow_unicode=True, sort_keys=False, explicit_start=True, ) output_path.write_text(yaml_content, encoding="utf-8") print(f"Generated Atomic Red Team YAML index at {output_path}") def generate_yaml_index_by_platform(self, output_path: Path, platform: str) -> None: """Generate a platform-specific YAML index.""" result: Dict[str, dict] = {} techniques_by_tactic = ATTACK_API.techniques_by_tactic() for tactic, techniques in techniques_by_tactic.items(): result[tactic] = {} for technique in techniques: tech_id = ATTACK_API.technique_identifier_for_technique(technique) # Create a copy of the technique and update name with display_name from YAML technique_copy = json.loads(json.dumps(technique)) # Deep copy atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) if atomic_yaml and atomic_yaml.get("display_name"): technique_copy["name"] = atomic_yaml["display_name"] result[tactic][tech_id] = { "technique": technique_copy, "atomic_tests": self.atomic_red_team.atomic_tests_for_technique_by_platform( technique, platform ), } yaml_content = yaml.dump( json.loads(json.dumps(result)), default_flow_style=False, allow_unicode=True, sort_keys=False, explicit_start=True, ) output_path.write_text(yaml_content, encoding="utf-8") print(f"Generated Atomic Red Team YAML index at {output_path}") def _get_layer(self, techniques: List[dict], layer_name: str) -> dict: """Create an ATT&CK Navigator layer structure.""" filters = {} if "Windows" in layer_name: filters = {"platforms": ["Windows"]} elif "macOS" in layer_name: filters = {"platforms": ["macOS"]} elif "Linux" in layer_name: filters = {"platforms": ["Linux"]} return { "name": layer_name, "versions": {"attack": "16", "navigator": "5.1.0", "layer": "4.5"}, "description": f"{layer_name} MITRE ATT&CK Navigator Layer", "domain": "enterprise-attack", "filters": filters, "gradient": { "colors": ["#ffffff", "#ce232e"], "minValue": 0, "maxValue": 10, }, "legendItems": [ {"label": "10 or more tests", "color": "#ce232e"}, {"label": "1 or more tests", "color": "#ffffff"}, ], "techniques": techniques, } def _update_techniques_list( self, current_technique: dict, current_technique_parent: dict, techniques_list: List[dict], atomic_yaml: dict, comments: bool, ) -> None: """Update the techniques list with a new technique.""" tech_id = atomic_yaml.get("attack_technique", "") if "." not in tech_id: # This is a parent technique tech_parent = next( ( t for t in techniques_list if t["techniqueID"] == tech_id.split(".")[0] ), None, ) if tech_parent: tech_parent["score"] += current_technique["score"] if comments: tech_parent["comment"] = current_technique.get("comment", "") else: if not comments: current_technique.pop("comment", None) techniques_list.append(current_technique) else: # This is a sub-technique parent_id = tech_id.split(".")[0] tech_parent = next( (t for t in techniques_list if t["techniqueID"] == parent_id), None ) if tech_parent: tech_parent["score"] += current_technique["score"] else: current_technique_parent["score"] += current_technique["score"] techniques_list.append(current_technique_parent) if not comments: current_technique.pop("comment", None) techniques_list.append(current_technique) def generate_navigator_layers(self, output_dir: Path) -> None: """Generate all ATT&CK Navigator layers.""" # Initialize technique lists for each platform platforms_data = { "all": [], "windows": [], "macos": [], "linux": [], "iaas": [], "iaas_aws": [], "iaas_azure": [], "iaas_gcp": [], "containers": [], "google_workspace": [], "azure_ad": [], "office_365": [], "esxi": [], } platform_patterns = { "windows": re.compile(r"windows", re.I), "macos": re.compile(r"macos", re.I), "linux": re.compile(r"linux", re.I), "iaas": re.compile(r"^iaas", re.I), "iaas_aws": re.compile(r"^iaas:aws", re.I), "iaas_azure": re.compile(r"^iaas:azure", re.I), "iaas_gcp": re.compile(r"^iaas:gcp", re.I), "containers": re.compile(r"^containers", re.I), "google_workspace": re.compile(r"^google-workspace", re.I), "azure_ad": re.compile(r"^azure-ad", re.I), "office_365": re.compile(r"^office-365", re.I), "esxi": re.compile(r"^esxi", re.I), } for atomic_yaml in self.atomic_red_team.atomic_tests: tech_id = atomic_yaml.get("attack_technique", "") base_technique = { "techniqueID": tech_id, "score": 0, "enabled": True, "comment": "\n", "links": [ { "label": "View Atomic", "url": f"https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/{tech_id}/{tech_id}.md", } ], } base_parent = { "techniqueID": tech_id.split(".")[0], "score": 0, "enabled": True, "links": [ { "label": "View Atomic", "url": f"https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/{tech_id.split('.')[0]}/{tech_id.split('.')[0]}.md", } ], } # Create platform-specific technique copies techniques = { key: {**base_technique, "comment": "\n"} for key in platforms_data } technique_parents = {key: {**base_parent} for key in platforms_data} has_tests = {key: False for key in platforms_data} for atomic in atomic_yaml.get("atomic_tests", []): techniques["all"]["score"] += 1 supported_platforms = atomic.get("supported_platforms", []) for platform_key, pattern in platform_patterns.items(): if any(pattern.match(p) for p in supported_platforms): has_tests[platform_key] = True techniques[platform_key]["score"] += 1 techniques[platform_key]["comment"] += f"- {atomic['name']}\n" # Update the all techniques list self._update_techniques_list( techniques["all"], technique_parents["all"], platforms_data["all"], atomic_yaml, False, ) # Update platform-specific lists for platform_key in platform_patterns: if has_tests[platform_key]: self._update_techniques_list( techniques[platform_key], technique_parents[platform_key], platforms_data[platform_key], atomic_yaml, True, ) # Write layers layer_configs = [ ("all", "art-navigator-layer.json", "Atomic Red Team"), ( "windows", "art-navigator-layer-windows.json", "Atomic Red Team (Windows)", ), ("macos", "art-navigator-layer-macos.json", "Atomic Red Team (macOS)"), ("linux", "art-navigator-layer-linux.json", "Atomic Red Team (Linux)"), ("iaas", "art-navigator-layer-iaas.json", "Atomic Red Team (Iaas)"), ( "iaas_aws", "art-navigator-layer-iaas-aws.json", "Atomic Red Team (Iaas:AWS)", ), ( "iaas_azure", "art-navigator-layer-iaas-azure.json", "Atomic Red Team (Iaas:Azure)", ), ( "iaas_gcp", "art-navigator-layer-iaas-gcp.json", "Atomic Red Team (Iaas:GCP)", ), ( "containers", "art-navigator-layer-containers.json", "Atomic Red Team (Containers)", ), ( "google_workspace", "art-navigator-layer-google-workspace.json", "Atomic Red Team (Google-Workspace)", ), ( "azure_ad", "art-navigator-layer-azure-ad.json", "Atomic Red Team (Azure-AD)", ), ( "office_365", "art-navigator-layer-office-365.json", "Atomic Red Team (Office-365)", ), ("esxi", "art-navigator-layer-esxi.json", "Atomic Red Team (ESXi)"), ] for platform_key, filename, layer_name in layer_configs: layer = self._get_layer(platforms_data[platform_key], layer_name) output_path = output_dir / filename # Use separators without spaces to match Ruby's compact JSON output output_path.write_text( json.dumps(layer, separators=(",", ":")), encoding="utf-8" ) print(f"Generated Atomic Red Team ATT&CK Navigator Layer at {output_path}") def generate_all_docs() -> Tuple[List[str], List[str]]: """Generate all Atomic Red Team documentation.""" return AtomicRedTeamDocs().generate_all_the_docs()