mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e68a5db3 | ||
|
|
2ea56755e6 |
No files matched your search
@@ -130,6 +130,10 @@ _DETAIL_PRIORITY_KEYS = (
|
||||
"finding_type",
|
||||
"cve",
|
||||
"cwe",
|
||||
"attack_techniques",
|
||||
"attack_source",
|
||||
"atlas_techniques",
|
||||
"atlas_source",
|
||||
"cvss",
|
||||
"filed_at",
|
||||
"target",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -543,6 +543,8 @@ def _result_properties(
|
||||
"method",
|
||||
"cve",
|
||||
"cwe",
|
||||
"attack_techniques",
|
||||
"atlas_techniques",
|
||||
"impact",
|
||||
"technical_analysis",
|
||||
"remediation_steps",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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]
|
||||
@@ -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"]
|
||||
Reference in new issue
Block a user