Files
2025-11-25 23:32:49 -05:00

176 lines
5.6 KiB
Python

import glob
import json
import os
import sys
import urllib.parse
from collections import defaultdict
from functools import partial
from pathlib import Path
from typing import Annotated, Optional
import typer
from pydantic import ValidationError
from atomic_red_team.common import atomics_path, used_guids_file
from atomic_red_team.guid import (
generate_guids_for_yaml,
get_unique_guid,
)
from atomic_red_team.labels import GithubAPI
from atomic_red_team.models import Technique
from atomic_red_team.utils import ATOMIC_RED_TEAM
from atomic_red_team.validator import Validator, format_validation_error, yaml
app = typer.Typer(help="Atomic Red Team Maintenance tool CLI helper")
@app.command()
def generate_guids():
"""Generates missing GUIDs for the atomic files"""
with open(used_guids_file, "r") as file:
used_guids = file.readlines()
for file in glob.glob(f"{atomics_path}/T*/T*.yaml"):
generate_guids_for_yaml(file, partial(get_unique_guid, guids=used_guids))
@app.command()
def generate_schemas():
"""Generates JSON and YAML schemas for techniques"""
schema = Technique.model_json_schema() # (1)!
with open("schema.yaml", "w") as f:
yaml.default_flow_style = False
yaml.dump(schema, f)
with open("schema.json", "w") as f:
f.write(json.dumps(schema, indent=2))
@app.command()
def generate_counter():
"""Generate atomic tests count svg"""
test_count = 0
for file in glob.glob(f"{atomics_path}/T*/T*.yaml"):
with open(file, "r") as f:
yaml_data = yaml.load(f)
if yaml_data is not None and "atomic_tests" in yaml_data:
test_count += len(yaml_data["atomic_tests"])
# Generate the shields.io badge URL
params = {"label": "Atomics", "message": str(test_count), "style": "flat"}
url = "https://img.shields.io/badge/{}-{}-{}.svg".format(
urllib.parse.quote_plus(params["label"]),
urllib.parse.quote_plus(params["message"]),
urllib.parse.quote_plus(params["style"]),
)
# Save shields URL in GitHub Output to be used in the next step.
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
print(f"result={url}", file=fh)
@app.command()
def generate_labels(
pull_request: Annotated[str, typer.Option("--pr")],
token: Annotated[str, typer.Option("--token")],
):
"""Generate labels for a pull request."""
api = GithubAPI(token)
api.save_labels_and_maintainers(pull_request)
@app.command()
def validate():
"""
Validate all the atomic techniques in a directory.
"""
validator = Validator()
errors = defaultdict(list)
for folder in glob.glob(f"{atomics_path}/T*"):
for item in os.scandir(folder):
try:
validator.validate(item)
except ValidationError as error:
errors[item.path].append(error)
if len(errors) == 0:
print("Validation successful")
else:
print("Validation failed")
for i, errors in errors.items():
print(f"Error occurred with {i.replace(f'{atomics_path}/', '')}.")
print("Each of the following are why it failed:")
for error in errors:
if isinstance(error, ValidationError):
for k, v in format_validation_error(error).items():
print(f"\n\tInvalid {'.'.join(map(str, v))}: {k}\n")
else:
print(f"\n\t{error}\n")
sys.exit(1)
@app.command()
def generate_docs(
technique_id: Annotated[
Optional[str],
typer.Option(
"--technique", "-t", help="Specific technique ID to generate docs for"
),
] = None,
output_dir: Annotated[
Optional[str],
typer.Option("--output", "-o", help="Output directory for documentation"),
] = None,
full: Annotated[
bool,
typer.Option("--full", "-f", help="Generate all docs including indexes, matrices, and navigator layers"),
] = False,
):
"""Generate Markdown documentation for atomic tests.
Use --full to generate all documentation including:
- Individual technique markdown files
- ATT&CK matrices (markdown)
- Platform-specific indexes (markdown, CSV, YAML)
- ATT&CK Navigator layers (JSON)
"""
if full:
# Generate all documentation including indexes
from atomic_red_team.doc_generator import generate_all_docs
oks, fails = generate_all_docs()
if fails:
sys.exit(len(fails))
return
if output_dir is None:
output_dir = atomics_path
if technique_id:
# Generate docs for a specific technique
technique_id = technique_id.upper()
output_path = Path(output_dir) / technique_id / f"{technique_id}.md"
try:
ATOMIC_RED_TEAM.generate_technique_docs(technique_id, str(output_path))
print(f"Generated documentation for {technique_id} at {output_path}")
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
else:
# Generate docs for all techniques
count = 0
for atomic_yaml in ATOMIC_RED_TEAM.atomic_tests:
tech_id = atomic_yaml.get("attack_technique", "").upper()
if tech_id:
output_path = Path(output_dir) / tech_id / f"{tech_id}.md"
try:
ATOMIC_RED_TEAM.generate_technique_docs(tech_id, str(output_path))
count += 1
except Exception as e:
print(f"Error generating docs for {tech_id}: {e}")
print(f"Generated documentation for {count} techniques")
if __name__ == "__main__":
app()