Compare commits

...
Author SHA1 Message Date
Alex Schapiro 58e68a5db3 feat(report): tag findings in AI systems with validated MITRE ATLAS techniques 2026-10-28 20:05:13 +00:00
Alex Schapiro 2ea56755e6 feat(report): tag findings with MITRE ATT&CK techniques
Reports accept an optional attack_techniques list, validated against a pinned
MITRE ATT&CK Enterprise v20.0 catalog of active techniques. The ids flow into
vulnerabilities.json, the markdown report, and the SARIF result properties.
2026-10-28 18:53:53 +00:00
12 changed files with 295 additions and 0 deletions

No files matched your search

+4
View File
@@ -130,6 +130,10 @@ _DETAIL_PRIORITY_KEYS = (
"finding_type",
"cve",
"cwe",
"attack_techniques",
"attack_source",
"atlas_techniques",
"atlas_source",
"cvss",
"filed_at",
"target",
+68
View File
@@ -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
+1
View File
@@ -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":{"naLine truncated
+70
View File
@@ -0,0 +1,70 @@
"""MITRE ATT&CK Enterprise technique ids for findings.
The catalog is a pinned snapshot of the active (not revoked, not deprecated)
Enterprise techniques, generated from MITRE's ``attack-stix-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"\bT\d{4}(?:\.\d{3})?\b", re.IGNORECASE)
@cache
def _catalog() -> dict[str, Any]:
raw = files("strix.report").joinpath("attack_catalog.json").read_text(encoding="utf-8")
data: dict[str, Any] = json.loads(raw)
return data
def attack_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://attack.mitre.org/techniques/{technique_id.replace('.', '/')}/"
def normalize_attack_techniques(raw: Any) -> tuple[list[str], list[str]]:
"""Parse technique 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 an
active Enterprise 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 ATT&CK technique: '{text}' (expected an id like 'T1190' or 'T1059.007')"
)
continue
for technique_id in found:
if technique_id in techniques:
continue
if technique(technique_id) is None:
errors.append(
f"unknown ATT&CK technique: '{technique_id}' is not an active "
f"MITRE ATT&CK Enterprise v{attack_version()} technique"
)
continue
techniques.append(technique_id)
return techniques, errors
+1
View File
@@ -0,0 +1 @@
{"domain":"enterprise-attack","version":"20.0","techniques":{"T1001":{"name":"Data Obfuscation","tactics":["command-and-control"]},"T1001.001":{"name":"Junk Data","tactics":["command-and-control"]},"T1001.002":{"name":"Steganography","tactics":["command-and-control"]},"T1001.003":{"name":"Protocol or Service Impersonation","tactics":["command-and-control"]},"T1003":{"name":"OS Credential Dumping","tactics":["credential-access"]},"T1003.001":{"name":"LSASS Memory","tactics":["credential-access"]},"T1003.002":{"name":"Security Account Manager","tactics":["credential-access"]},"T1003.003":{"name":"NTDS","tactics":["credential-access"]},"T1003.004":{"name":"LSA Secrets","tactics":["credential-access"]},"T1003.005":{"name":"Cached Domain Credentials","tactics":["credential-access"]},"T1003.006":{"name":"DCSync","tactics":["credential-access"]},"T1003.007":{"name":"Proc Filesystem","tactics":["credential-access"]},"T1003.008":{"name":"/etc/passwd and /etc/shadow","tactics":["credential-access"]},"T1005":{"name":"Data from Local System","tactics":["collection"]},"T1006":{"name":"Direct Volume Access","tactics":["stealth"]},"T1007":{"name":"System Service Discovery","tactics":["discovery"]},"T1008":{"name":"Fallback Channels","tactics":["command-and-control"]},"T1010":{"name":"Application Window Discovery","tactics":["discovery"]},"T1011":{"name":"Exfiltration Over Other Network Medium","tactics":["exfiltration"]},"T1011.001":{"name":"Exfiltration Over Bluetooth","tactics":["exfiltration"]},"T1012":{"name":"Query Registry","tactics":["discovery"]},"T1014":{"name":"Rootkit","tactics":["stealth"]},"T1016":{"name":"System Network Configuration Discovery","tactics":["discovery"]},"T1016.001":{"name":"Internet Connection Discovery","tactics":["discovery"]},"T1016.002":{"name":"Wi-Fi Discovery","tactics":["discovery"]},"T1018":{"name":"Remote System Discovery","tactics":["discovery"]},"T1020":{"name":"Automated Exfiltration","tactics":["exfiltration"]},"T1020.001":{"name":"Traffic Duplication","tactics":["exfiltration"]},"T1021":{"name":"Remote Services","tactics":["lateral-movement"]},"T1021.001":{"name":"Remote Desktop Protocol","tactics":["lateral-movement"]},"T1021.002":{"name":"SMB/Windows Admin Shares","tactics":["lateral-movement"]},"T1021.003":{"name":"Distributed Component Object Model","tactics":["lateral-movement"]},"T1021.004":{"name":"SSH","tactics":["lateral-movement"]},"T1021.005":{"name":"VNC","tactics":["lateral-movement"]},"T1021.006":{"name":"Windows Remote Management","tactics":["lateral-movement"]},"T1021.007":{"name":"Cloud Services","tactics":["lateral-movement"]},"T1021.008":{"name":"Direct Cloud VM Connections","tactics":["lateral-movement"]},"T1025":{"name":"Data from Removable Media","tactics":["collection"]},"T1027":{"name":"Obfuscated Files or Information","tactics":["stealth"]},"T1027.001":{"name":"Binary Padding","tactics":["stealth"]},"T1027.002":{"name":"Software Packing","tactics":["stealth"]},"T1027.003":{"name":"Steganography","tactics":["stealth"]},"T1027.004":{"name":"Compile After Delivery","tactics":["stealth"]},"T1027.005":{"name":"Indicator Removal from Tools","tactics":["stealth"]},"T1027.006":{"name":"HTML Smuggling","tactics":["stealth"]},"T1027.007":{"name":"Dynamic API Resolution","tactics":["stealth"]},"T1027.008":{"name":"Stripped Payloads","tactics":["stealth"]},"T1027.009":{"name":"Embedded Payloads","tactics":["stealth"]},"T1027.010":{"name":"Command Obfuscation","tactics":["stealth"]},"T1027.011":{"name":"Fileless Storage","tactics":["stealth"]},"T1027.012":{"name":"LNK Icon Smuggling","tactics":["stealth"]},"T1027.013":{"name":"Encrypted/Encoded File","tactics":["stealth"]},"T1027.014":{"name":"Polymorphic Code","tactics":["stealth"]},"T1027.015":{"name":"Compression","tactics":["stealth"]},"T1027.016":{"name":"Junk Code Insertion","tactics":["stealth"]},"T1027.017":{"name":"SVG Smuggling","tactics":["stealth"]},"T1027.018":{"name":"Invisible Unicode","tactics":["stealth"]},"T1029":{"name":"Scheduled Transfer","tactics":["exfiltration"]},"T1030":{"name":"Data Transfer Size Limits","tactics":["exfiltration"]},"T1033":{"name":"System Owner/User Discovery","tactics":["discovery"]},"T1036":{"name":"Masquerading","tactics":["stealth"]},"T1036.001":{"name":"Invalid Code Signature","tactics":["stealth"]},"T1036.002":{"name":"Right-to-Left Override","tactics":["stealth"]},"T1036.003":{"name":"Rename Legitimate Utilities","tactics":["stealth"]},"T1036.004":{"name":"Masquerade Task or Service","tactics":["stealth"]},"T1036.005":{"name":"Match Legitimate Resource Name or Location","tactics":["stealth"]},"T1036.006":{"name":"Space after Filename","tactics":["stealth"]},"T1036.007":{"name":"Double File Extension","tactics":["stealth"]},"T1036.008":{"name":"Masquerade File Type","tactics":["stealth"]},"T1036.009":{"name":"Break Process Trees","tactics":["stealth"]},"T1036.010":{"name":"Masquerade Account Name","tactics":["stealth"]},"T1036.011":{"name":"Overwrite Process ArgumentsLine truncated
+2
View File
@@ -543,6 +543,8 @@ def _result_properties(
"method",
"cve",
"cwe",
"attack_techniques",
"atlas_techniques",
"impact",
"technical_analysis",
"remediation_steps",
+8
View File
@@ -91,6 +91,8 @@ UPDATABLE_REPORT_FIELDS = frozenset(
"method",
"cve",
"cwe",
"attack_techniques",
"atlas_techniques",
"code_locations",
"http_exchange_ids",
"fix_verification",
@@ -353,6 +355,8 @@ class ReportState:
method: str | None = None,
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,
@@ -411,6 +415,10 @@ class ReportState:
report["cve"] = cve.strip()
if cwe:
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:
+2
View File
@@ -241,6 +241,8 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
("Method", report.get("method")),
("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:
+59
View File
@@ -16,6 +16,8 @@ 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
@@ -462,6 +464,20 @@ def _collect_update_changes( # noqa: PLR0912, PLR0915
if cwe:
changes["cwe"] = cwe
if fields.get("attack_techniques") is not None:
attack_techniques, attack_errors = normalize_attack_techniques(
fields.get("attack_techniques")
)
errors.extend(attack_errors)
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)
@@ -803,6 +819,8 @@ async def _do_create(
confidence_rationale: str | None = None,
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]:
@@ -845,6 +863,10 @@ async def _do_create(
errors.extend(_validate_fix_verification(parsed_locations, fix_verification))
cve, cwe, identifier_errors = _validate_identifiers(cve, cwe)
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
)
@@ -907,6 +929,8 @@ async def _do_create(
"method": method,
"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,
@@ -1000,6 +1024,8 @@ async def create_vulnerability_report(
method: str | None = None,
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,
@@ -1296,6 +1322,18 @@ async def create_vulnerability_report(
method: HTTP method when relevant.
cve: ``CVE-YYYY-NNNNN`` if certain, else omit.
cwe: ``CWE-NNN`` (most specific child) if certain, else omit.
attack_techniques: MITRE ATT&CK Enterprise technique ids (e.g.
``["T1190", "T1552.001"]``) for what the PoC actually did:
the entry point it exploited and each step it demonstrated
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``.
@@ -1450,6 +1488,8 @@ async def create_vulnerability_report(
method=method,
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,
@@ -1485,6 +1525,8 @@ async def update_vulnerability_report(
method: str | None = None,
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,
@@ -1558,6 +1600,9 @@ async def update_vulnerability_report(
method: Replacement HTTP method.
cve: Replacement CVE id.
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.
@@ -1604,6 +1649,8 @@ async def update_vulnerability_report(
"method": method,
"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,
@@ -1905,6 +1952,7 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
fix_verification: str | None = None,
contextual_cvss_breakdown: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None,
attack_techniques: list[str] | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
) -> dict[str, Any]:
@@ -1935,6 +1983,9 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
if cwe_err:
errors.append(cwe_err)
normalized_attack_techniques, attack_errors = normalize_attack_techniques(attack_techniques)
errors.extend(attack_errors)
fix_effort = (fix_effort or "").strip().lower()
if fix_effort not in _VALID_FIX_EFFORT:
errors.append(
@@ -2061,6 +2112,7 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
cvss=cvss_score if advisory_cvss is not None else None,
cve=parsed_cve,
cwe=cwe,
attack_techniques=normalized_attack_techniques or None,
code_locations=parsed_locations,
fix_verification=fix_verification,
finding_class="dependency_cve",
@@ -2121,6 +2173,7 @@ async def create_dependency_report(
fix_verification: str | None = None,
contextual_cvss_breakdown: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None,
attack_techniques: list[str] | None = None,
) -> str:
"""File a known-CVE dependency (SCA) finding — one report per CVE x package.
@@ -2185,6 +2238,10 @@ async def create_dependency_report(
package_ecosystem: e.g. ``npm`` / ``pypi`` / ``maven`` / ``go``.
fixed_version: First non-vulnerable version, if known.
cwe: ``CWE-NNN`` (most specific) if certain, else omit.
attack_techniques: MITRE ATT&CK Enterprise technique ids the
reachability evidence demonstrates (e.g. ``["T1190"]`` for a
proven exploitable path from a public endpoint). Omit when the
evidence does not show one.
advisory_cvss: **Required.** Published advisory base score
(0.0-10.0) — read it off the advisory (trivy CVSS / NVD / GHSA).
It is the published reference the finding is rated against and
@@ -2310,6 +2367,7 @@ async def create_dependency_report(
fix_verification=fix_verification,
contextual_cvss_breakdown=contextual_cvss_breakdown,
contextual_cvss_reasoning=contextual_cvss_reasoning,
attack_techniques=attack_techniques,
agent_id=agent_id,
agent_name=agent_name,
)
@@ -2338,6 +2396,7 @@ _REPORT_SUMMARY_FIELDS = (
"finding_class",
"cve",
"cwe",
"attack_techniques",
"target",
"endpoint",
"method",
+19
View File
@@ -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"
+54
View File
@@ -2408,3 +2408,57 @@ async def test_delete_tool_carries_the_caller_identity(report_state: ReportState
assert history[0]["agent_id"] == "aaaa1111"
assert history[0]["agent_name"] == "Recon Agent"
assert "update_vulnerability_report" in delete_vulnerability_report.description
async def test_create_records_attack_techniques(report_state: ReportState) -> None:
result = await _do_create(
**_CONFIRMED_KWARGS,
attack_techniques=["t1190", "T1552.001 - Credentials In Files", "T1190"],
)
assert result["success"] is True
assert report_state.vulnerability_reports[0]["attack_techniques"] == ["T1190", "T1552.001"]
async def test_create_rejects_unknown_attack_technique(report_state: ReportState) -> None:
result = await _do_create(**_CONFIRMED_KWARGS, attack_techniques=["T9999", "exploit"])
assert result["success"] is False
assert any("unknown ATT&CK technique: 'T9999'" in error for error in result["errors"])
assert any("invalid ATT&CK technique: 'exploit'" in error for error in result["errors"])
assert report_state.vulnerability_reports == []
async def test_create_without_attack_techniques_omits_the_field(
report_state: ReportState,
) -> None:
result = await _do_create(**_CONFIRMED_KWARGS)
assert result["success"] is True
assert "attack_techniques" not in report_state.vulnerability_reports[0]
def test_update_replaces_attack_techniques(report_state: ReportState) -> None:
_seed_weak_report(report_state)
result = _do_update(
report_id="vuln-0009",
update_reason="The chained PoC read cloud credentials.",
fields={"attack_techniques": ["T1190", "T1552.005"]},
)
assert result["success"] is True
assert report_state.vulnerability_reports[0]["attack_techniques"] == ["T1190", "T1552.005"]
def test_update_rejects_revoked_attack_technique(report_state: ReportState) -> None:
_seed_weak_report(report_state)
result = _do_update(
report_id="vuln-0009",
update_reason="Tag the technique.",
fields={"attack_techniques": ["T1086"]},
)
assert result["success"] is False
assert "attack_techniques" not in report_state.vulnerability_reports[0]
+7
View File
@@ -371,3 +371,10 @@ def test_calibration_metadata_survives_into_result_properties(tmp_path: Path) ->
assert strix["confidence_rationale"] == "Reproduced once out of three attempts."
assert strix["severity_change_conditions"] == "Critical if the WAF rule is removed."
assert strix["fix_verification"] == "Not retested."
def test_write_sarif_carries_attack_techniques(tmp_path: Path) -> None:
write_sarif(tmp_path, [_finding(attack_techniques=["T1190", "T1059"])])
result = _read(tmp_path)["runs"][0]["results"][0]
assert result["properties"]["strix"]["attack_techniques"] == ["T1190", "T1059"]