diff --git a/strix/interface/cloud/render.py b/strix/interface/cloud/render.py index 0e4603733..da5d78462 100644 --- a/strix/interface/cloud/render.py +++ b/strix/interface/cloud/render.py @@ -132,6 +132,8 @@ _DETAIL_PRIORITY_KEYS = ( "cwe", "attack_techniques", "attack_source", + "atlas_techniques", + "atlas_source", "cvss", "filed_at", "target", diff --git a/strix/report/atlas.py b/strix/report/atlas.py new file mode 100644 index 000000000..cad2e64b1 --- /dev/null +++ b/strix/report/atlas.py @@ -0,0 +1,68 @@ +"""MITRE ATLAS technique ids for findings in AI systems (LLM apps, agents, RAG). + +The catalog is a pinned snapshot of the ATLAS techniques, generated from +MITRE's ``atlas-data`` release. +""" + +from __future__ import annotations + +import json +import re +from functools import cache +from importlib.resources import files +from typing import Any + + +_TECHNIQUE_ID = re.compile(r"\bAML\.T\d{4}(?:\.\d{3})?\b", re.IGNORECASE) + + +@cache +def _catalog() -> dict[str, Any]: + raw = files("strix.report").joinpath("atlas_catalog.json").read_text(encoding="utf-8") + data: dict[str, Any] = json.loads(raw) + return data + + +def atlas_version() -> str: + return str(_catalog()["version"]) + + +def technique(technique_id: str) -> dict[str, Any] | None: + entry = _catalog()["techniques"].get(technique_id) + return entry if isinstance(entry, dict) else None + + +def technique_url(technique_id: str) -> str: + return f"https://atlas.mitre.org/techniques/{technique_id}" + + +def normalize_atlas_techniques(raw: Any) -> tuple[list[str], list[str]]: + """Parse ATLAS ids from a list or string and check them against the catalog. + + Returns the ids in first-seen order and one error per id that is not a + known ATLAS technique. + """ + if raw is None: + return [], [] + items = raw if isinstance(raw, list) else [raw] + techniques: list[str] = [] + errors: list[str] = [] + for item in items: + text = str(item or "").strip() + if not text: + continue + found = [match.upper() for match in _TECHNIQUE_ID.findall(text)] + if not found: + errors.append(f"invalid ATLAS technique: '{text}' (expected an id like 'AML.T0051')") + continue + for technique_id in found: + if technique_id in techniques: + continue + if technique(technique_id) is None: + errors.append( + f"unknown ATLAS technique: '{technique_id}' is not a " + f"MITRE ATLAS v{atlas_version()} technique" + ) + continue + techniques.append(technique_id) + return techniques, errors diff --git a/strix/report/atlas_catalog.json b/strix/report/atlas_catalog.json new file mode 100644 index 000000000..29de6551e --- /dev/null +++ b/strix/report/atlas_catalog.json @@ -0,0 +1 @@ +{"version":"2026.09","techniques":{"AML.T0000":{"name":"Search Open Technical Databases"},"AML.T0000.000":{"name":"Journals and Conference Proceedings"},"AML.T0000.001":{"name":"Pre-Print Repositories"},"AML.T0000.002":{"name":"Technical Blogs"},"AML.T0000.003":{"name":"Scan Databases"},"AML.T0001":{"name":"Search Open AI Vulnerability Analysis"},"AML.T0002":{"name":"Acquire Public AI Artifacts"},"AML.T0002.000":{"name":"Datasets"},"AML.T0002.001":{"name":"Models"},"AML.T0002.002":{"name":"AI Agent Configuration"},"AML.T0003":{"name":"Search Victim-Owned Websites"},"AML.T0004":{"name":"Search Application Repositories"},"AML.T0005":{"name":"Create Proxy AI Model"},"AML.T0005.000":{"name":"Train Proxy via Gathered AI Artifacts"},"AML.T0005.001":{"name":"Train Proxy via Replication"},"AML.T0005.002":{"name":"Use Pre-Trained Model"},"AML.T0006":{"name":"Active Scanning"},"AML.T0006.000":{"name":"Enumerate Hosted AI Resources"},"AML.T0006.001":{"name":"Query Platform Metadata APIs"},"AML.T0006.002":{"name":"Scan for Exposed AI Infrastructure"},"AML.T0006.003":{"name":"Probe AI Agent Trigger Channels"},"AML.T0007":{"name":"Discover AI Artifacts"},"AML.T0008":{"name":"Acquire Infrastructure"},"AML.T0008.000":{"name":"AI Development Workspaces"},"AML.T0008.001":{"name":"Consumer Hardware"},"AML.T0008.002":{"name":"Domains"},"AML.T0008.003":{"name":"Physical Countermeasures"},"AML.T0008.004":{"name":"Serverless"},"AML.T0008.005":{"name":"AI Service Proxies"},"AML.T0010":{"name":"AI Supply Chain Compromise"},"AML.T0010.000":{"name":"Hardware"},"AML.T0010.001":{"name":"AI Software"},"AML.T0010.002":{"name":"Data"},"AML.T0010.003":{"name":"Model"},"AML.T0010.004":{"name":"Container Registry"},"AML.T0010.005":{"name":"AI Agent Tool"},"AML.T0011":{"name":"User Execution"},"AML.T0011.000":{"name":"Unsafe AI Artifacts"},"AML.T0011.001":{"name":"Malicious Package"},"AML.T0011.002":{"name":"Poisoned AI Agent Tool"},"AML.T0011.003":{"name":"Malicious Link"},"AML.T0012":{"name":"Valid Accounts"},"AML.T0013":{"name":"Discover AI Model Ontology"},"AML.T0014":{"name":"Discover AI Model Family"},"AML.T0015":{"name":"Evade AI Model"},"AML.T0016":{"name":"Obtain Capabilities"},"AML.T0016.000":{"name":"Adversarial AI Attack Implementations"},"AML.T0016.001":{"name":"Software Tools"},"AML.T0016.002":{"name":"Generative AI"},"AML.T0016.003":{"name":"Exploits"},"AML.T0016.004":{"name":"AI Agent Tools"},"AML.T0017":{"name":"Develop Capabilities"},"AML.T0017.000":{"name":"Adversarial AI Attacks"},"AML.T0017.001":{"name":"Autonomous Exploit Development"},"AML.T0017.002":{"name":"AI Agent Tools"},"AML.T0018":{"name":"Manipulate AI Model"},"AML.T0018.000":{"name":"Poison AI Model"},"AML.T0018.001":{"name":"Modify AI Model Architecture"},"AML.T0018.002":{"name":"Embed Malware"},"AML.T0018.003":{"name":"Modify Prompt Construction Logic"},"AML.T0020":{"name":"Training Data Poisoning"},"AML.T0021":{"name":"Establish Accounts"},"AML.T0024":{"name":"Exfiltration via AI Inference API"},"AML.T0024.000":{"name":"Infer Training Data Membership"},"AML.T0024.001":{"name":"Invert AI Model"},"AML.T0024.002":{"name":"Extract AI Model"},"AML.T0025":{"name":"Exfiltration via Cyber Means"},"AML.T0029":{"name":"Denial of AI Service"},"AML.T0031":{"name":"Erode AI Model Integrity"},"AML.T0034":{"name":"Cost Harvesting"},"AML.T0034.000":{"name":"Excessive Queries"},"AML.T0034.001":{"name":"Resource-Intensive Queries"},"AML.T0034.002":{"name":"Agentic Resource Consumption"},"AML.T0035":{"name":"AI Artifact Collection"},"AML.T0036":{"name":"Data from Information Repositories"},"AML.T0037":{"name":"Data from Local System"},"AML.T0040":{"name":"AI Model Inference API Access"},"AML.T0041":{"name":"Physical Environment Access"},"AML.T0042":{"name":"Verify Attack"},"AML.T0043":{"name":"Craft Adversarial Data"},"AML.T0043.000":{"name":"White-Box Optimization"},"AML.T0043.001":{"name":"Black-Box Optimization"},"AML.T0043.002":{"name":"Black-Box Transfer"},"AML.T0043.003":{"name":"Manual Modification"},"AML.T0043.004":{"name":"Insert Backdoor Trigger"},"AML.T0044":{"name":"Full AI Model Access"},"AML.T0046":{"name":"Spamming AI System with Chaff Data"},"AML.T0047":{"name":"AI-Enabled Product or Service"},"AML.T0048":{"name":"External Harms"},"AML.T0048.000":{"name":"Financial Harm"},"AML.T0048.001":{"name":"Reputational Harm"},"AML.T0048.002":{"name":"Societal Harm"},"AML.T0048.003":{"name":"User Harm"},"AML.T0048.004":{"name":"AI Intellectual Property Theft"},"AML.T0049":{"name":"Exploit Public-Facing Application"},"AML.T0050":{"name":"Command and Scripting Interpreter"},"AML.T0051":{"name":"LLM Prompt Injection"},"AML.T0051.000":{"name":"Direct"},"AML.T0051.001":{"name":"Indirect"},"AML.T0051.002":{"name":"Triggered"},"AML.T0052":{"name":"Phishing"},"AML.T0052.000":{"name":"Spearphishing via Social Engineering LLM"},"AML.T0052.001":{"name":"Deepfake-Assisted Phishing"},"AML.T0053":{"name":"AI Agent Tool Invocation"},"AML.T0054":{"name":"LLM Jailbreak"},"AML.T0055":{"name":"Unsecured Credentials"},"AML.T0056":{"name":"Extract LLM System Prompt"},"AML.T0057":{"name":"LLM Data Leakage"},"AML.T0059":{"name":"Erode Dataset Integrity"},"AML.T0060":{"name":"Publish Hallucinated Entities"},"AML.T0061":{"name":"LLM Prompt Self-Replication"},"AML.T0062":{"name":"Discover LLM Hallucinations"},"AML.T0063":{"name":"Discover AI Model Outputs"},"AML.T0064":{"name":"Gather RAG-Indexed Targets"},"AML.T0065":{"name":"LLM Prompt Crafting"},"AML.T0066":{"name":"Retrieval Content Crafting"},"AML.T0067":{"name":"LLM Trusted Output Components Manipulation"},"AML.T0067.000":{"name":"Citations"},"AML.T0068":{"name":"LLM Prompt Obfuscation"},"AML.T0069":{"name":"Discover LLM System Information"},"AML.T0069.000":{"name":"Special Character Sets"},"AML.T0069.001":{"name":"System Instruction Keywords"},"AML.T0069.002":{"name":"System Prompt"},"AML.T0070":{"name":"RAG Poisoning"},"AML.T0071":{"name":"False RAG Entry Injection"},"AML.T0072":{"name":"Cyber Communication Channel"},"AML.T0073":{"name":"Impersonation"},"AML.T0074":{"name":"Masquerading"},"AML.T0075":{"name":"Enterprise Resource Discovery"},"AML.T0076":{"name":"Corrupt AI Model"},"AML.T0077":{"name":"LLM Response Rendering"},"AML.T0078":{"name":"Drive-by Compromise"},"AML.T0079":{"name":"Stage Capabilities"},"AML.T0080":{"name":"AI Agent Context Poisoning"},"AML.T0080.000":{"name":"Memory"},"AML.T0080.001":{"name":"Thread"},"AML.T0081":{"name":"Modify AI Agent Configuration"},"AML.T0082":{"name":"RAG Credential Harvesting"},"AML.T0083":{"name":"Credentials from AI Agent Configuration"},"AML.T0084":{"name":"Discover AI Agent Configuration"},"AML.T0084.000":{"name":"Embedded Knowledge"},"AML.T0084.001":{"name":"Tool Definitions"},"AML.T0084.002":{"name":"Activation Triggers"},"AML.T0084.003":{"name":"Call Chains"},"AML.T0085":{"name":"Data from AI Services"},"AML.T0085.000":{"name":"RAG Databases"},"AML.T0085.001":{"name":"AI Agent Tools"},"AML.T0086":{"name":"Exfiltration via AI Agent Tool Invocation"},"AML.T0087":{"name":"Gather Victim Identity Information"},"AML.T0088":{"name":"Generate Deepfakes"},"AML.T0089":{"name":"Enterprise Environment Discovery"},"AML.T0090":{"name":"OS Credential Dumping"},"AML.T0091":{"name":"Use Alternate Authentication Material"},"AML.T0091.000":{"name":"Application Access Token"},"AML.T0091.001":{"name":"Web Session Cookie"},"AML.T0092":{"name":"Manipulate User LLM Chat History"},"AML.T0093":{"name":"Prompt Infiltration via Public-Facing Application"},"AML.T0094":{"name":"Delay Execution of LLM Instructions"},"AML.T0095":{"name":"Search Open Websites/Domains"},"AML.T0095.000":{"name":"Code Repositories"},"AML.T0096":{"name":"AI Service API"},"AML.T0097":{"name":"Virtualization/Sandbox Evasion"},"AML.T0098":{"name":"AI Agent Tool Credential Harvesting"},"AML.T0099":{"name":"AI Agent Tool Data Poisoning"},"AML.T0100":{"name":"AI Agent Clickbait"},"AML.T0101":{"name":"Data Destruction via AI Agent Tool Invocation"},"AML.T0102":{"name":"Generate Malicious Commands"},"AML.T0103":{"name":"Deploy AI Agent"},"AML.T0105":{"name":"Escape to Host"},"AML.T0106":{"name":"Exploitation for Credential Access"},"AML.T0107":{"name":"Exploitation for Defense Evasion"},"AML.T0108":{"name":"AI Agent"},"AML.T0109":{"name":"AI Supply Chain Rug Pull"},"AML.T0110":{"name":"AI Agent Tool Poisoning"},"AML.T0110.000":{"name":"Definition and Instructions"},"AML.T0110.001":{"name":"Implementation"},"AML.T0110.002":{"name":"Runtime Response"},"AML.T0111":{"name":"AI Supply Chain Reputation Inflation"},"AML.T0112":{"name":"Machine Compromise"},"AML.T0112.000":{"name":"Local AI Agent"},"AML.T0112.001":{"name":"AI Artifacts"},"AML.T0113":{"name":"Steal Web Session Cookie"},"AML.T0114":{"name":"AI Service Web Interface"},"AML.T0115":{"name":"Publish Poisoned AI Artifacts"},"AML.T0115.000":{"name":"Datasets"},"AML.T0115.001":{"name":"Models"},"AML.T0115.002":{"name":"AI Agent Tools"},"AML.T0116":{"name":"Autonomous Reconnaissance"},"AML.T0117":{"name":"Autonomous Attack-Path Adaptation"},"AML.T0118":{"name":"Autonomous AI Agent Communication"},"AML.T0118.000":{"name":"Communication via Shared Artifacts"},"AML.T0118.001":{"name":"Direct Agent Communication"},"AML.T0119":{"name":"Exploit Automated Artifact Processing Pipeline"},"AML.T0120":{"name":"AI Artifact Repository"},"AML.T0121":{"name":"AI Agent Environment Reconstruction"},"AML.T0122":{"name":"Exploitation of Remote Services"},"AML.T0123":{"name":"Obfuscated Files or Information"},"AML.T0124":{"name":"Autonomous Attack Orchestration"},"AML.T0125":{"name":"Create Account"},"AML.T0126":{"name":"Automated Collection"},"AML.T0127":{"name":"Data Staged"},"AML.T0128":{"name":"Compromise Infrastructure"},"AML.T0129":{"name":"Triggers in Multimodal Inputs"},"AML.T0130":{"name":"AI Agent Response Biasing"},"AML.T0131":{"name":"Crafted AI Assistant Links"},"AML.T0132":{"name":"Misconfigured or Publicly Exposed AI Services"},"AML.T0133":{"name":"Discover AI Agent Runtime Capabilities"},"AML.T0134":{"name":"AI Targeted Cloaking"}}} diff --git a/strix/report/sarif.py b/strix/report/sarif.py index 188191e07..0a971a906 100644 --- a/strix/report/sarif.py +++ b/strix/report/sarif.py @@ -544,6 +544,7 @@ def _result_properties( "cve", "cwe", "attack_techniques", + "atlas_techniques", "impact", "technical_analysis", "remediation_steps", diff --git a/strix/report/state.py b/strix/report/state.py index 6f8869088..1fc3c6926 100644 --- a/strix/report/state.py +++ b/strix/report/state.py @@ -92,6 +92,7 @@ UPDATABLE_REPORT_FIELDS = frozenset( "cve", "cwe", "attack_techniques", + "atlas_techniques", "code_locations", "http_exchange_ids", "fix_verification", @@ -355,6 +356,7 @@ class ReportState: cve: str | None = None, cwe: str | None = None, attack_techniques: list[str] | None = None, + atlas_techniques: list[str] | None = None, code_locations: list[dict[str, Any]] | None = None, http_exchange_ids: list[str] | None = None, fix_verification: str | None = None, @@ -415,6 +417,8 @@ class ReportState: report["cwe"] = cwe.strip() if attack_techniques: report["attack_techniques"] = list(attack_techniques) + if atlas_techniques: + report["atlas_techniques"] = list(atlas_techniques) if code_locations: report["code_locations"] = code_locations if http_exchange_ids: diff --git a/strix/report/writer.py b/strix/report/writer.py index 55a14c9d3..cfaa097d3 100644 --- a/strix/report/writer.py +++ b/strix/report/writer.py @@ -242,6 +242,7 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL ("CVE", report.get("cve")), ("CWE", report.get("cwe")), ("MITRE ATT&CK", ", ".join(report.get("attack_techniques") or [])), + ("MITRE ATLAS", ", ".join(report.get("atlas_techniques") or [])), ] cvss = report.get("cvss") if cvss is not None: diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 1503401e1..3141bbd25 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any from agents import RunContextWrapper, function_tool +from strix.report.atlas import normalize_atlas_techniques from strix.report.attack import normalize_attack_techniques from strix.tools.nullish import clean_optional from strix.tools.proxy.tools import existing_request_ids @@ -471,6 +472,12 @@ def _collect_update_changes( # noqa: PLR0912, PLR0915 if not attack_errors: changes["attack_techniques"] = attack_techniques + if fields.get("atlas_techniques") is not None: + atlas_techniques, atlas_errors = normalize_atlas_techniques(fields.get("atlas_techniques")) + errors.extend(atlas_errors) + if not atlas_errors: + changes["atlas_techniques"] = atlas_techniques + raw_http_exchange_ids = fields.get("http_exchange_ids") http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids(raw_http_exchange_ids) errors.extend(http_exchange_errors) @@ -813,6 +820,7 @@ async def _do_create( fix_verification: str | None = None, fix_pr_body: str | None = None, attack_techniques: list[str] | None = None, + atlas_techniques: list[str] | None = None, agent_id: str | None = None, agent_name: str | None = None, ) -> dict[str, Any]: @@ -857,6 +865,8 @@ async def _do_create( errors.extend(identifier_errors) normalized_attack_techniques, attack_errors = normalize_attack_techniques(attack_techniques) errors.extend(attack_errors) + normalized_atlas_techniques, atlas_errors = normalize_atlas_techniques(atlas_techniques) + errors.extend(atlas_errors) normalized_http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids( http_exchange_ids ) @@ -920,6 +930,7 @@ async def _do_create( "cve": cve, "cwe": cwe, "attack_techniques": normalized_attack_techniques or None, + "atlas_techniques": normalized_atlas_techniques or None, "code_locations": parsed_locations, "fix_verification": fix_verification, "fix_pr_body": fix_pr_body, @@ -1014,6 +1025,7 @@ async def create_vulnerability_report( cve: str | None = None, cwe: str | None = None, attack_techniques: list[str] | None = None, + atlas_techniques: list[str] | None = None, code_locations: list[dict[str, Any]] | None = None, http_exchange_ids: list[str] | None = None, confidence_rationale: str | None = None, @@ -1316,6 +1328,12 @@ async def create_vulnerability_report( after that (credentials it read, commands it ran, data it took). Tag only techniques the evidence shows; leave out techniques the finding merely makes possible. Omit when unsure. + atlas_techniques: MITRE ATLAS technique ids (e.g. ``["AML.T0051"]``) + when the target is an AI system (LLM app, agent, RAG pipeline) + and the PoC showed the attack, for example prompt injection + (AML.T0051), jailbreak (AML.T0054), system prompt extraction + (AML.T0056) or data leakage (AML.T0057). Same rule: only what the + evidence shows. Omit for other findings. code_locations: White-box findings — list of location objects. http_exchange_ids: Proxy request IDs that prove this finding. Copy these IDs from ``list_requests`` or ``view_request``. @@ -1471,6 +1489,7 @@ async def create_vulnerability_report( cve=cve, cwe=cwe, attack_techniques=attack_techniques, + atlas_techniques=atlas_techniques, code_locations=code_locations, http_exchange_ids=http_exchange_ids, fix_verification=fix_verification, @@ -1507,6 +1526,7 @@ async def update_vulnerability_report( cve: str | None = None, cwe: str | None = None, attack_techniques: list[str] | None = None, + atlas_techniques: list[str] | None = None, code_locations: list[dict[str, Any]] | None = None, http_exchange_ids: list[str] | None = None, fix_verification: str | None = None, @@ -1582,6 +1602,7 @@ async def update_vulnerability_report( cwe: Replacement CWE id. attack_techniques: Replacement MITRE ATT&CK technique ids. Same rule as on create: only what the evidence demonstrates. + atlas_techniques: Replacement MITRE ATLAS technique ids. Same rule. code_locations: Replacement code locations. http_exchange_ids: Replacement proxy request ids. Pass an empty list to remove all linked exchanges. @@ -1629,6 +1650,7 @@ async def update_vulnerability_report( "cve": cve, "cwe": cwe, "attack_techniques": attack_techniques, + "atlas_techniques": atlas_techniques, "code_locations": code_locations, "http_exchange_ids": http_exchange_ids, "fix_verification": fix_verification, diff --git a/tests/test_atlas.py b/tests/test_atlas.py new file mode 100644 index 000000000..8eedc582a --- /dev/null +++ b/tests/test_atlas.py @@ -0,0 +1,19 @@ +from strix.report.atlas import normalize_atlas_techniques, technique_url + + +def test_normalize_atlas_techniques_validates_ids() -> None: + ids, errors = normalize_atlas_techniques(["aml.t0051", "AML.T0051", "AML.T0054 jailbreak"]) + assert ids == ["AML.T0051", "AML.T0054"] + assert errors == [] + + +def test_normalize_atlas_techniques_rejects_unknown_and_malformed() -> None: + ids, errors = normalize_atlas_techniques(["AML.T9999", "T1190"]) + assert ids == [] + assert len(errors) == 2 + assert "unknown ATLAS technique" in errors[0] + assert "invalid ATLAS technique" in errors[1] + + +def test_technique_url() -> None: + assert technique_url("AML.T0051.000") == "https://atlas.mitre.org/techniques/AML.T0051.000"