refactor(docs+comments): add Google-style docstrings and inline comments across backend
Task D — Google-style docstrings (Args/Returns) on every public function, method, and class across all 158 Python files in the backend. Zero ruff D violations (pydocstyle Google convention). Task E — Explanatory one-line comment before every code line (~11600 new comments). ruff check passes clean after isort re-sort.
This commit is contained in:
@@ -1,26 +1,41 @@
|
||||
"""D3FEND import service — fetches MITRE D3FEND data and creates
|
||||
DefensiveTechnique records plus ATT&CK → D3FEND mappings.
|
||||
"""D3FEND import service — fetches MITRE D3FEND data and creates DefensiveTechnique records plus ATT&CK → D3FEND mappings.
|
||||
|
||||
Uses the D3FEND public API:
|
||||
- https://d3fend.mitre.org/api/technique/api-all.json (all defensive techniques)
|
||||
- https://d3fend.mitre.org/api/offensive-technique/{attack_id}.json (mappings per ATT&CK technique)
|
||||
"""
|
||||
|
||||
# Import logging
|
||||
import logging
|
||||
|
||||
# Import Any from typing
|
||||
from typing import Any
|
||||
|
||||
# Import UUID from uuid
|
||||
from uuid import UUID
|
||||
|
||||
# Import httpx
|
||||
import httpx
|
||||
|
||||
# Import Session from sqlalchemy.orm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import DefensiveTechnique, DefensiveTechniqueMapping from app.models.defensive_technique
|
||||
from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping
|
||||
|
||||
# Import Technique from app.models.technique
|
||||
from app.models.technique import Technique
|
||||
|
||||
# Assign logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Assign D3FEND_TACTIC_URL = "https://d3fend.mitre.org/api/tactic/d3f:{tactic}.json"
|
||||
D3FEND_TACTIC_URL = "https://d3fend.mitre.org/api/tactic/d3f:{tactic}.json"
|
||||
# Assign D3FEND_MAPPING_URL = "https://d3fend.mitre.org/api/offensive-technique/{attack_id}.json"
|
||||
D3FEND_MAPPING_URL = "https://d3fend.mitre.org/api/offensive-technique/{attack_id}.json"
|
||||
# Assign D3FEND_BASE_URL = "https://d3fend.mitre.org/technique/d3f:{iri}"
|
||||
D3FEND_BASE_URL = "https://d3fend.mitre.org/technique/d3f:{iri}"
|
||||
# Assign D3FEND_TACTICS = ["Detect", "Harden", "Isolate", "Deceive", "Evict", "Model"]
|
||||
D3FEND_TACTICS = ["Detect", "Harden", "Isolate", "Deceive", "Evict", "Model"]
|
||||
|
||||
|
||||
@@ -28,121 +43,228 @@ D3FEND_TACTICS = ["Detect", "Harden", "Isolate", "Deceive", "Evict", "Model"]
|
||||
|
||||
|
||||
def _to_str(v: Any) -> str: # noqa: ANN401
|
||||
"""Coerce an RDF value (str, dict with @value, or list) to a plain string."""
|
||||
"""Coerce an RDF value (str, dict with @value, or list) to a plain string.
|
||||
|
||||
Args:
|
||||
v (Any): RDF node value — may be a plain string, a dict containing
|
||||
a ``@value`` key, or a list of such values.
|
||||
|
||||
Returns:
|
||||
str: Plain string representation; ``"; "``-joined for list inputs.
|
||||
"""
|
||||
# Check: isinstance(v, dict)
|
||||
if isinstance(v, dict):
|
||||
# Return v.get("@value", str(v))
|
||||
return v.get("@value", str(v))
|
||||
# Check: isinstance(v, list)
|
||||
if isinstance(v, list):
|
||||
# Return "; ".join(_to_str(x) for x in v)
|
||||
return "; ".join(_to_str(x) for x in v)
|
||||
# Return str(v) if v else ""
|
||||
return str(v) if v else ""
|
||||
|
||||
|
||||
# Define function _fetch_techniques_from_tactic_apis
|
||||
def _fetch_techniques_from_tactic_apis() -> list[dict[str, Any]]:
|
||||
"""Fetch all defensive techniques via D3FEND tactic APIs.
|
||||
|
||||
Uses ``/api/tactic/d3f:{tactic}.json`` which is reliable and returns
|
||||
full metadata including the ontology IRI for each technique.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: Deduplicated list of technique dicts, each
|
||||
containing ``d3fend_id``, ``iri``, ``name``, ``description``,
|
||||
and ``tactic``.
|
||||
"""
|
||||
# Assign all_techniques = []
|
||||
all_techniques: list[dict[str, Any]] = []
|
||||
# Assign seen = set()
|
||||
seen: set[str] = set()
|
||||
|
||||
# Open context manager
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
# Iterate over D3FEND_TACTICS
|
||||
for tactic in D3FEND_TACTICS:
|
||||
# Assign url = D3FEND_TACTIC_URL.format(tactic=tactic)
|
||||
url = D3FEND_TACTIC_URL.format(tactic=tactic)
|
||||
# Attempt the following; catch errors below
|
||||
try:
|
||||
# Assign resp = client.get(url)
|
||||
resp = client.get(url)
|
||||
# Call resp.raise_for_status()
|
||||
resp.raise_for_status()
|
||||
# Assign data = resp.json()
|
||||
data = resp.json()
|
||||
# Handle Exception
|
||||
except Exception as e:
|
||||
# Log warning: "Failed to fetch D3FEND tactic %s: %s", tactic, e
|
||||
logger.warning("Failed to fetch D3FEND tactic %s: %s", tactic, e)
|
||||
# Skip to the next loop iteration
|
||||
continue
|
||||
|
||||
# Assign graph = data.get("techniques", {}).get("@graph", [])
|
||||
graph = data.get("techniques", {}).get("@graph", [])
|
||||
# Iterate over graph
|
||||
for node in graph:
|
||||
# Assign nid = node.get("@id", "")
|
||||
nid = node.get("@id", "")
|
||||
# Assign d3id = _to_str(node.get("d3f:d3fend-id", ""))
|
||||
d3id = _to_str(node.get("d3f:d3fend-id", ""))
|
||||
# Assign label = _to_str(node.get("rdfs:label", ""))
|
||||
label = _to_str(node.get("rdfs:label", ""))
|
||||
# Assign defn = _to_str(node.get("d3f:definition", ""))
|
||||
defn = _to_str(node.get("d3f:definition", ""))
|
||||
# Check: not defn
|
||||
if not defn:
|
||||
# Assign defn = _to_str(node.get("rdfs:comment", ""))
|
||||
defn = _to_str(node.get("rdfs:comment", ""))
|
||||
|
||||
# Assign iri = nid.replace("d3f:", "") if nid.startswith("d3f:") else nid
|
||||
iri = nid.replace("d3f:", "") if nid.startswith("d3f:") else nid
|
||||
|
||||
# Check: d3id and label and d3id not in seen
|
||||
if d3id and label and d3id not in seen:
|
||||
# Call seen.add()
|
||||
seen.add(d3id)
|
||||
# Call all_techniques.append()
|
||||
all_techniques.append({
|
||||
# Literal argument value
|
||||
"d3fend_id": d3id,
|
||||
# Literal argument value
|
||||
"iri": iri,
|
||||
# Literal argument value
|
||||
"name": label,
|
||||
# Literal argument value
|
||||
"description": defn[:500] if defn else None,
|
||||
# Literal argument value
|
||||
"tactic": tactic,
|
||||
})
|
||||
|
||||
# Log info: "D3FEND tactic %s: %d techniques", tactic, len(gra
|
||||
logger.info("D3FEND tactic %s: %d techniques", tactic, len(graph))
|
||||
|
||||
# Return all_techniques
|
||||
return all_techniques
|
||||
|
||||
|
||||
# Define function _upsert_techniques
|
||||
def _upsert_techniques(db: Session, techniques: list[dict[str, Any]]) -> dict[str, int]:
|
||||
"""Upsert a list of technique dicts into the DefensiveTechnique table."""
|
||||
"""Upsert a list of technique dicts into the DefensiveTechnique table.
|
||||
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
techniques (list[dict[str, Any]]): List of technique data dicts, each
|
||||
containing ``d3fend_id``, ``name``, and optionally ``description``,
|
||||
``tactic``, and ``iri``.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: Contains ``created``, ``updated``, and ``total``
|
||||
counts after the upsert.
|
||||
"""
|
||||
# Assign created = 0
|
||||
created = 0
|
||||
# Assign updated = 0
|
||||
updated = 0
|
||||
|
||||
# Iterate over techniques
|
||||
for tech_data in techniques:
|
||||
# Assign existing = (
|
||||
existing = (
|
||||
db.query(DefensiveTechnique)
|
||||
# Chain .filter() call
|
||||
.filter(DefensiveTechnique.d3fend_id == tech_data["d3fend_id"])
|
||||
# Chain .first() call
|
||||
.first()
|
||||
)
|
||||
# Assign iri = tech_data.get("iri") or tech_data["name"].replace(" ", "")
|
||||
iri = tech_data.get("iri") or tech_data["name"].replace(" ", "")
|
||||
# Assign d3fend_url = D3FEND_BASE_URL.format(iri=iri)
|
||||
d3fend_url = D3FEND_BASE_URL.format(iri=iri)
|
||||
|
||||
# Check: existing
|
||||
if existing:
|
||||
# Assign existing.name = tech_data["name"]
|
||||
existing.name = tech_data["name"]
|
||||
# Assign existing.description = tech_data.get("description")
|
||||
existing.description = tech_data.get("description")
|
||||
# Assign existing.tactic = tech_data.get("tactic")
|
||||
existing.tactic = tech_data.get("tactic")
|
||||
# Assign existing.d3fend_url = d3fend_url
|
||||
existing.d3fend_url = d3fend_url
|
||||
# Assign updated = 1
|
||||
updated += 1
|
||||
# Fallback: handle remaining cases
|
||||
else:
|
||||
# Assign new_tech = DefensiveTechnique(
|
||||
new_tech = DefensiveTechnique(
|
||||
# Keyword argument: d3fend_id
|
||||
d3fend_id=tech_data["d3fend_id"],
|
||||
# Keyword argument: name
|
||||
name=tech_data["name"],
|
||||
# Keyword argument: description
|
||||
description=tech_data.get("description"),
|
||||
# Keyword argument: tactic
|
||||
tactic=tech_data.get("tactic"),
|
||||
# Keyword argument: d3fend_url
|
||||
d3fend_url=d3fend_url,
|
||||
)
|
||||
# Stage new record(s) for database insertion
|
||||
db.add(new_tech)
|
||||
# Assign created = 1
|
||||
created += 1
|
||||
|
||||
# Commit all pending changes to the database
|
||||
db.commit()
|
||||
# Assign total = db.query(DefensiveTechnique).count()
|
||||
total = db.query(DefensiveTechnique).count()
|
||||
# Return {"created": created, "updated": updated, "total": total}
|
||||
return {"created": created, "updated": updated, "total": total}
|
||||
|
||||
|
||||
# Define function import_d3fend_techniques
|
||||
def import_d3fend_techniques(db: Session) -> dict[str, int]:
|
||||
"""Fetch all D3FEND defensive techniques and upsert into DB.
|
||||
|
||||
Uses the tactic-level APIs which are reliable and provide full metadata
|
||||
including ontology IRIs for correct URL generation.
|
||||
|
||||
Returns a dict with counts: {created, updated, total}.
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: Contains ``created``, ``updated``, and ``total``
|
||||
counts; falls back to curated list when the API returns fewer
|
||||
than 50 techniques.
|
||||
"""
|
||||
# Log info: "Fetching D3FEND techniques from tactic APIs"
|
||||
logger.info("Fetching D3FEND techniques from tactic APIs")
|
||||
|
||||
# Attempt the following; catch errors below
|
||||
try:
|
||||
# Assign techniques = _fetch_techniques_from_tactic_apis()
|
||||
techniques = _fetch_techniques_from_tactic_apis()
|
||||
# Handle Exception
|
||||
except Exception as e:
|
||||
# Log error: "Failed to fetch D3FEND techniques from tactic API
|
||||
logger.error("Failed to fetch D3FEND techniques from tactic APIs: %s", e)
|
||||
# Assign techniques = []
|
||||
techniques = []
|
||||
|
||||
# Check: len(techniques) >= 50
|
||||
if len(techniques) >= 50:
|
||||
# Log info: "Fetched %d D3FEND techniques from tactic APIs", l
|
||||
logger.info("Fetched %d D3FEND techniques from tactic APIs", len(techniques))
|
||||
# Assign result = _upsert_techniques(db, techniques)
|
||||
result = _upsert_techniques(db, techniques)
|
||||
# Log info: "D3FEND import done: %d created, %d updated, %d to
|
||||
logger.info("D3FEND import done: %d created, %d updated, %d total",
|
||||
result["created"], result["updated"], result["total"])
|
||||
# Return result
|
||||
return result
|
||||
|
||||
# Fallback: use a curated list of well-known D3FEND techniques
|
||||
logger.warning("Tactic APIs returned too few techniques (%d), using fallback", len(techniques))
|
||||
# Return _import_d3fend_fallback(db)
|
||||
return _import_d3fend_fallback(db)
|
||||
|
||||
|
||||
@@ -228,9 +350,20 @@ _FALLBACK_TECHNIQUES: list[dict[str, str | None]] = [
|
||||
]
|
||||
|
||||
|
||||
# Define function _import_d3fend_fallback
|
||||
def _import_d3fend_fallback(db: Session) -> dict[str, int]:
|
||||
"""Import curated D3FEND techniques when the tactic APIs are unreachable."""
|
||||
"""Import curated D3FEND techniques when the tactic APIs are unreachable.
|
||||
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: Contains ``created``, ``updated``, and ``total``
|
||||
counts from upserting the fallback technique list.
|
||||
"""
|
||||
# Log info: "Using fallback D3FEND technique list (%d entries
|
||||
logger.info("Using fallback D3FEND technique list (%d entries)", len(_FALLBACK_TECHNIQUES))
|
||||
# Return _upsert_techniques(db, _FALLBACK_TECHNIQUES) # type: ignore[arg-type]
|
||||
return _upsert_techniques(db, _FALLBACK_TECHNIQUES) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@@ -239,218 +372,399 @@ def _import_d3fend_fallback(db: Session) -> dict[str, int]:
|
||||
|
||||
# Curated ATT&CK → D3FEND mapping for common techniques
|
||||
_ATTACK_TO_D3FEND: dict[str, list[str]] = {
|
||||
# Literal argument value
|
||||
"T1059": ["D3-PSA", "D3-SCA", "D3-PA", "D3-EAW", "D3-EDL", "D3-PLA"],
|
||||
# Literal argument value
|
||||
"T1059.001": ["D3-PSA", "D3-SCA", "D3-PA", "D3-EAW", "D3-EDL"],
|
||||
# Literal argument value
|
||||
"T1059.003": ["D3-PSA", "D3-SCA", "D3-PA", "D3-EAW"],
|
||||
# Literal argument value
|
||||
"T1059.005": ["D3-PSA", "D3-SCA", "D3-EAW"],
|
||||
# Literal argument value
|
||||
"T1059.007": ["D3-PSA", "D3-SCA", "D3-EAW"],
|
||||
# Literal argument value
|
||||
"T1055": ["D3-PA", "D3-PSA", "D3-HBPI", "D3-PMAD", "D3-PLA"],
|
||||
# Literal argument value
|
||||
"T1055.001": ["D3-PA", "D3-PMAD", "D3-HBPI"],
|
||||
# Literal argument value
|
||||
"T1055.002": ["D3-PA", "D3-PMAD", "D3-HBPI"],
|
||||
# Literal argument value
|
||||
"T1003": ["D3-CH", "D3-CR", "D3-MFA", "D3-PMAD"],
|
||||
# Literal argument value
|
||||
"T1003.001": ["D3-CH", "D3-CR", "D3-PMAD"],
|
||||
# Literal argument value
|
||||
"T1078": ["D3-MFA", "D3-UBA", "D3-UGLPA", "D3-CH"],
|
||||
# Literal argument value
|
||||
"T1078.001": ["D3-MFA", "D3-UBA", "D3-CH"],
|
||||
# Literal argument value
|
||||
"T1566": ["D3-EAL", "D3-FA", "D3-FH", "D3-UA", "D3-EHR"],
|
||||
# Literal argument value
|
||||
"T1566.001": ["D3-EAL", "D3-FA", "D3-FH", "D3-EHR"],
|
||||
# Literal argument value
|
||||
"T1566.002": ["D3-UA", "D3-EAL", "D3-EHR"],
|
||||
# Literal argument value
|
||||
"T1071": ["D3-AL", "D3-NTA", "D3-PM", "D3-CT"],
|
||||
# Literal argument value
|
||||
"T1071.001": ["D3-AL", "D3-NTA", "D3-PM"],
|
||||
# Literal argument value
|
||||
"T1053": ["D3-PSA", "D3-PA", "D3-SCHE", "D3-SSA"],
|
||||
# Literal argument value
|
||||
"T1053.005": ["D3-PSA", "D3-SCHE", "D3-SSA"],
|
||||
# Literal argument value
|
||||
"T1543": ["D3-SMRA", "D3-SSA", "D3-SBAN"],
|
||||
# Literal argument value
|
||||
"T1543.003": ["D3-SMRA", "D3-SSA", "D3-SBAN"],
|
||||
# Literal argument value
|
||||
"T1547": ["D3-SICA", "D3-SSA", "D3-RRID"],
|
||||
# Literal argument value
|
||||
"T1547.001": ["D3-SICA", "D3-SSA", "D3-RRID"],
|
||||
# Literal argument value
|
||||
"T1021": ["D3-RTSD", "D3-RPA", "D3-NTA", "D3-MFA"],
|
||||
# Literal argument value
|
||||
"T1021.001": ["D3-RTSD", "D3-NTA", "D3-MFA"],
|
||||
# Literal argument value
|
||||
"T1021.002": ["D3-RTSD", "D3-NTA", "D3-NI"],
|
||||
# Literal argument value
|
||||
"T1560": ["D3-FA", "D3-FCA", "D3-ORA"],
|
||||
# Literal argument value
|
||||
"T1560.001": ["D3-FA", "D3-FCA"],
|
||||
# Literal argument value
|
||||
"T1048": ["D3-ORA", "D3-NTA", "D3-OTF"],
|
||||
# Literal argument value
|
||||
"T1048.003": ["D3-ORA", "D3-NTA", "D3-OTF"],
|
||||
# Literal argument value
|
||||
"T1105": ["D3-IRA", "D3-NTA", "D3-FA", "D3-FH"],
|
||||
# Literal argument value
|
||||
"T1036": ["D3-FCA", "D3-FH", "D3-FA", "D3-SWI"],
|
||||
# Literal argument value
|
||||
"T1036.005": ["D3-FCA", "D3-FH", "D3-FA"],
|
||||
# Literal argument value
|
||||
"T1140": ["D3-FA", "D3-DA", "D3-SCA"],
|
||||
# Literal argument value
|
||||
"T1070": ["D3-SSA", "D3-LOGA", "D3-SYSM"],
|
||||
# Literal argument value
|
||||
"T1070.004": ["D3-SSA", "D3-FAPA"],
|
||||
# Literal argument value
|
||||
"T1562": ["D3-SSA", "D3-SYSM", "D3-SMRA"],
|
||||
# Literal argument value
|
||||
"T1562.001": ["D3-SSA", "D3-SYSM", "D3-SMRA"],
|
||||
# Literal argument value
|
||||
"T1027": ["D3-DA", "D3-FA", "D3-RE"],
|
||||
# Literal argument value
|
||||
"T1027.002": ["D3-DA", "D3-FA"],
|
||||
# Literal argument value
|
||||
"T1110": ["D3-MFA", "D3-UBA", "D3-CH"],
|
||||
# Literal argument value
|
||||
"T1110.001": ["D3-MFA", "D3-UBA", "D3-CH"],
|
||||
# Literal argument value
|
||||
"T1082": ["D3-PSA", "D3-PA", "D3-SYSM"],
|
||||
# Literal argument value
|
||||
"T1083": ["D3-FAPA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1497": ["D3-DA", "D3-SE"],
|
||||
# Literal argument value
|
||||
"T1218": ["D3-PSA", "D3-PLA", "D3-EAW"],
|
||||
# Literal argument value
|
||||
"T1218.011": ["D3-PSA", "D3-PLA", "D3-EAW"],
|
||||
# Literal argument value
|
||||
"T1569": ["D3-SMRA", "D3-PSA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1569.002": ["D3-SMRA", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1012": ["D3-RRID", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1112": ["D3-RRID", "D3-PA", "D3-REGG"],
|
||||
# Literal argument value
|
||||
"T1057": ["D3-PA", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1518": ["D3-SYSM", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1049": ["D3-NTA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1016": ["D3-NTA", "D3-PA", "D3-SYSM"],
|
||||
# Literal argument value
|
||||
"T1033": ["D3-PA", "D3-UBA"],
|
||||
# Literal argument value
|
||||
"T1087": ["D3-UBA", "D3-PA", "D3-SSA"],
|
||||
# Literal argument value
|
||||
"T1087.001": ["D3-UBA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1087.002": ["D3-UBA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1018": ["D3-NTA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1047": ["D3-RPA", "D3-PSA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1190": ["D3-ISVA", "D3-NTA", "D3-AL"],
|
||||
# Literal argument value
|
||||
"T1133": ["D3-NTA", "D3-MFA", "D3-RTSD"],
|
||||
# Literal argument value
|
||||
"T1486": ["D3-BKUP", "D3-FBKP", "D3-ANTR", "D3-FA"],
|
||||
# Literal argument value
|
||||
"T1490": ["D3-BKUP", "D3-FBKP", "D3-SSA"],
|
||||
# Literal argument value
|
||||
"T1489": ["D3-SMRA", "D3-SSA"],
|
||||
# Literal argument value
|
||||
"T1098": ["D3-UBA", "D3-SSA", "D3-PGOV"],
|
||||
# Literal argument value
|
||||
"T1136": ["D3-UBA", "D3-SSA", "D3-UACM"],
|
||||
# Literal argument value
|
||||
"T1136.001": ["D3-UBA", "D3-SSA", "D3-UACM"],
|
||||
# Literal argument value
|
||||
"T1068": ["D3-SU", "D3-VULM", "D3-HBPI"],
|
||||
# Literal argument value
|
||||
"T1548": ["D3-PSEP", "D3-PSA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1548.002": ["D3-PSEP", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1134": ["D3-PA", "D3-PSA", "D3-PSEP"],
|
||||
# Literal argument value
|
||||
"T1134.001": ["D3-PA", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1574": ["D3-SWI", "D3-FCA", "D3-PLA"],
|
||||
# Literal argument value
|
||||
"T1574.001": ["D3-SWI", "D3-FCA"],
|
||||
# Literal argument value
|
||||
"T1204": ["D3-EAL", "D3-FA", "D3-UA"],
|
||||
# Literal argument value
|
||||
"T1204.001": ["D3-UA", "D3-EAL"],
|
||||
# Literal argument value
|
||||
"T1204.002": ["D3-FA", "D3-EAL", "D3-DA"],
|
||||
# Literal argument value
|
||||
"T1071.004": ["D3-DPM", "D3-DNSSM", "D3-NTA"],
|
||||
# Literal argument value
|
||||
"T1571": ["D3-NTA", "D3-PM", "D3-AL"],
|
||||
# Literal argument value
|
||||
"T1572": ["D3-NTA", "D3-AL", "D3-PM"],
|
||||
# Literal argument value
|
||||
"T1041": ["D3-ORA", "D3-NTA"],
|
||||
# Literal argument value
|
||||
"T1005": ["D3-FAPA", "D3-PA"],
|
||||
# Literal argument value
|
||||
"T1113": ["D3-PA", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1056": ["D3-PA", "D3-PSA", "D3-HBPI"],
|
||||
# Literal argument value
|
||||
"T1056.001": ["D3-PA", "D3-PSA"],
|
||||
# Literal argument value
|
||||
"T1560.003": ["D3-FA", "D3-ORA"],
|
||||
# Literal argument value
|
||||
"T1583": ["D3-IPMR", "D3-DNSRA"],
|
||||
# Literal argument value
|
||||
"T1584": ["D3-IPMR", "D3-DNSRA"],
|
||||
# Literal argument value
|
||||
"T1595": ["D3-IRA", "D3-NTA"],
|
||||
# Literal argument value
|
||||
"T1589": ["D3-UBA", "D3-THRT"],
|
||||
# Literal argument value
|
||||
"T1590": ["D3-NTA", "D3-THRT"],
|
||||
# Literal argument value
|
||||
"T1591": ["D3-THRT"],
|
||||
# Literal argument value
|
||||
"T1592": ["D3-THRT"],
|
||||
}
|
||||
|
||||
|
||||
# Define function import_d3fend_mappings
|
||||
def import_d3fend_mappings(db: Session) -> dict[str, int]:
|
||||
"""Create ATT&CK → D3FEND mappings.
|
||||
|
||||
First tries the D3FEND API for each ATT&CK technique in the DB,
|
||||
then falls back to the curated mapping for any remaining techniques.
|
||||
|
||||
Returns a dict with counts: {created, skipped, total}.
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: Contains ``created``, ``skipped``, and ``total``
|
||||
mapping counts.
|
||||
"""
|
||||
# Assign created = 0
|
||||
created = 0
|
||||
# Assign skipped = 0
|
||||
skipped = 0
|
||||
|
||||
# Get all ATT&CK techniques from the DB
|
||||
attack_techniques = db.query(Technique).all()
|
||||
# Assign technique_map = {t.mitre_id: t for t in attack_techniques}
|
||||
technique_map = {t.mitre_id: t for t in attack_techniques}
|
||||
|
||||
# Get all defensive techniques
|
||||
defensive_techniques = db.query(DefensiveTechnique).all()
|
||||
# Assign d3fend_map = {dt.d3fend_id: dt for dt in defensive_techniques}
|
||||
d3fend_map = {dt.d3fend_id: dt for dt in defensive_techniques}
|
||||
|
||||
# Check: not d3fend_map
|
||||
if not d3fend_map:
|
||||
# Log warning: "No D3FEND techniques in DB — run import_d3fend_te
|
||||
logger.warning("No D3FEND techniques in DB — run import_d3fend_techniques first")
|
||||
# Return {"created": 0, "skipped": 0, "total": 0}
|
||||
return {"created": 0, "skipped": 0, "total": 0}
|
||||
|
||||
# Use the curated mapping for now (API per-technique is very slow for 700+ techniques)
|
||||
for mitre_id, d3fend_ids in _ATTACK_TO_D3FEND.items():
|
||||
# Assign attack_tech = technique_map.get(mitre_id)
|
||||
attack_tech = technique_map.get(mitre_id)
|
||||
# Check: not attack_tech
|
||||
if not attack_tech:
|
||||
# Skip to the next loop iteration
|
||||
continue
|
||||
|
||||
# Iterate over d3fend_ids
|
||||
for d3fend_id in d3fend_ids:
|
||||
# Assign def_tech = d3fend_map.get(d3fend_id)
|
||||
def_tech = d3fend_map.get(d3fend_id)
|
||||
# Check: not def_tech
|
||||
if not def_tech:
|
||||
# Skip to the next loop iteration
|
||||
continue
|
||||
|
||||
# Check if mapping already exists
|
||||
existing = (
|
||||
db.query(DefensiveTechniqueMapping)
|
||||
# Chain .filter() call
|
||||
.filter(
|
||||
DefensiveTechniqueMapping.attack_technique_id == attack_tech.id,
|
||||
DefensiveTechniqueMapping.defensive_technique_id == def_tech.id,
|
||||
)
|
||||
# Chain .first() call
|
||||
.first()
|
||||
)
|
||||
|
||||
# Check: existing
|
||||
if existing:
|
||||
# Assign skipped = 1
|
||||
skipped += 1
|
||||
# Skip to the next loop iteration
|
||||
continue
|
||||
|
||||
# Assign mapping = DefensiveTechniqueMapping(
|
||||
mapping = DefensiveTechniqueMapping(
|
||||
# Keyword argument: attack_technique_id
|
||||
attack_technique_id=attack_tech.id,
|
||||
# Keyword argument: defensive_technique_id
|
||||
defensive_technique_id=def_tech.id,
|
||||
)
|
||||
# Stage new record(s) for database insertion
|
||||
db.add(mapping)
|
||||
# Assign created = 1
|
||||
created += 1
|
||||
|
||||
# Commit all pending changes to the database
|
||||
db.commit()
|
||||
|
||||
# Assign total = db.query(DefensiveTechniqueMapping).count()
|
||||
total = db.query(DefensiveTechniqueMapping).count()
|
||||
# Log info: "D3FEND mappings: %d created, %d skipped, %d total
|
||||
logger.info("D3FEND mappings: %d created, %d skipped, %d total", created, skipped, total)
|
||||
# Return {"created": created, "skipped": skipped, "total": total}
|
||||
return {"created": created, "skipped": skipped, "total": total}
|
||||
|
||||
|
||||
# Define function sync
|
||||
def sync(db: Session) -> dict:
|
||||
"""Sync D3FEND techniques and ATT&CK mappings.
|
||||
|
||||
Called by the Data Sources router when the user clicks Sync for D3FEND.
|
||||
Returns a flat summary dict suitable for ``last_sync_stats``.
|
||||
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
|
||||
Returns:
|
||||
dict: Flat summary dict suitable for ``last_sync_stats``, containing
|
||||
``techniques_created``, ``techniques_updated``,
|
||||
``techniques_total``, ``mappings_created``,
|
||||
``mappings_skipped``, and ``mappings_total``.
|
||||
"""
|
||||
# Import datetime from datetime
|
||||
from datetime import datetime
|
||||
|
||||
# Import DataSource from app.models.data_source
|
||||
from app.models.data_source import DataSource
|
||||
|
||||
# Assign tech_result = import_d3fend_techniques(db)
|
||||
tech_result = import_d3fend_techniques(db)
|
||||
# Assign mapping_result = import_d3fend_mappings(db)
|
||||
mapping_result = import_d3fend_mappings(db)
|
||||
|
||||
# Assign summary = {
|
||||
summary = {
|
||||
# Literal argument value
|
||||
"techniques_created": tech_result.get("created", 0),
|
||||
# Literal argument value
|
||||
"techniques_updated": tech_result.get("updated", 0),
|
||||
# Literal argument value
|
||||
"techniques_total": tech_result.get("total", 0),
|
||||
# Literal argument value
|
||||
"mappings_created": mapping_result.get("created", 0),
|
||||
# Literal argument value
|
||||
"mappings_skipped": mapping_result.get("skipped", 0),
|
||||
# Literal argument value
|
||||
"mappings_total": mapping_result.get("total", 0),
|
||||
}
|
||||
|
||||
# Update DataSource record
|
||||
ds = db.query(DataSource).filter(DataSource.name == "d3fend").first()
|
||||
# Check: ds
|
||||
if ds:
|
||||
# Assign ds.last_sync_at = datetime.utcnow()
|
||||
ds.last_sync_at = datetime.utcnow()
|
||||
# Assign ds.last_sync_status = "success"
|
||||
ds.last_sync_status = "success"
|
||||
# Assign ds.last_sync_stats = summary
|
||||
ds.last_sync_stats = summary
|
||||
# Commit all pending changes to the database
|
||||
db.commit()
|
||||
|
||||
# Log info: "D3FEND sync complete — %s", summary
|
||||
logger.info("D3FEND sync complete — %s", summary)
|
||||
# Return summary
|
||||
return summary
|
||||
|
||||
|
||||
# Define function get_defenses_for_technique
|
||||
def get_defenses_for_technique(db: Session, technique_id: UUID) -> list[dict]:
|
||||
"""Get all D3FEND defensive techniques mapped to a given ATT&CK technique."""
|
||||
"""Return all D3FEND defensive techniques mapped to a given ATT&CK technique.
|
||||
|
||||
Args:
|
||||
db (Session): Active SQLAlchemy database session.
|
||||
technique_id (UUID): UUID of the ATT&CK technique to look up.
|
||||
|
||||
Returns:
|
||||
list[dict]: List of defensive technique dicts, each containing
|
||||
``id``, ``d3fend_id``, ``name``, ``description``, ``tactic``,
|
||||
and ``d3fend_url``.
|
||||
"""
|
||||
# Assign mappings = (
|
||||
mappings = (
|
||||
db.query(DefensiveTechniqueMapping)
|
||||
# Chain .filter() call
|
||||
.filter(DefensiveTechniqueMapping.attack_technique_id == technique_id)
|
||||
# Chain .all() call
|
||||
.all()
|
||||
)
|
||||
|
||||
# Assign results = []
|
||||
results = []
|
||||
# Iterate over mappings
|
||||
for m in mappings:
|
||||
# Assign dt = m.defensive_technique
|
||||
dt = m.defensive_technique
|
||||
# Call results.append()
|
||||
results.append({
|
||||
# Literal argument value
|
||||
"id": str(dt.id),
|
||||
# Literal argument value
|
||||
"d3fend_id": dt.d3fend_id,
|
||||
# Literal argument value
|
||||
"name": dt.name,
|
||||
# Literal argument value
|
||||
"description": dt.description,
|
||||
# Literal argument value
|
||||
"tactic": dt.tactic,
|
||||
# Literal argument value
|
||||
"d3fend_url": dt.d3fend_url,
|
||||
})
|
||||
|
||||
# Return results
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user