fix(threat-actors): fix 500 on search + populate motivation from STIX
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
1. fix(search 500): func.cast(col, func.text()) is invalid SQLAlchemy — replaced with cast(col, Text) for both aliases and target_sectors JSONB columns. Generating correct CAST(col AS TEXT) SQL. 2. feat(motivation): extract primary_motivation and sophistication from STIX intrusion-set objects during MITRE sync. Added _normalize_motivation() to map STIX vocabulary → simplified frontend values (espionage / financial / destruction / hacktivism). Both create and update paths now set these fields. Run MITRE sync to backfill existing actors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,38 @@ def _extract_mitre_url(external_references: list) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
# Map STIX primary_motivation vocabulary → simplified frontend values
|
||||
_MOTIVATION_MAP: dict[str, str] = {
|
||||
# Espionage / nation-state
|
||||
"espionage": "espionage",
|
||||
"national-security": "espionage",
|
||||
"political": "espionage",
|
||||
# Financial
|
||||
"financial": "financial",
|
||||
"financial-gain": "financial",
|
||||
"personal-gain": "financial",
|
||||
"organizational-gain": "financial",
|
||||
# Destruction / disruption
|
||||
"destruction": "destruction",
|
||||
"disruption": "destruction",
|
||||
"coercion": "destruction",
|
||||
"dominance": "destruction",
|
||||
# Hacktivism / ideology
|
||||
"ideology": "hacktivism",
|
||||
"hacktivism": "hacktivism",
|
||||
"notoriety": "hacktivism",
|
||||
"personal-satisfaction": "hacktivism",
|
||||
"revenge": "hacktivism",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_motivation(raw: str | None) -> str | None:
|
||||
"""Normalize a STIX primary_motivation value to the Aegis vocabulary."""
|
||||
if not raw:
|
||||
return None
|
||||
return _MOTIVATION_MAP.get(raw.lower().strip())
|
||||
|
||||
|
||||
def _parse_intrusion_sets(objects: list) -> list[dict]:
|
||||
"""Parse STIX intrusion-set objects into ThreatActor dicts."""
|
||||
actors = []
|
||||
@@ -139,6 +171,11 @@ def _parse_intrusion_sets(objects: list) -> list[dict]:
|
||||
|
||||
description = obj.get("description", "")
|
||||
|
||||
# Extract primary_motivation and sophistication from STIX object
|
||||
raw_motivation = obj.get("primary_motivation")
|
||||
motivation = _normalize_motivation(raw_motivation)
|
||||
sophistication = obj.get("sophistication") # e.g. "advanced", "expert"
|
||||
|
||||
# Extract references (non-MITRE)
|
||||
references = []
|
||||
for ref in ext_refs:
|
||||
@@ -159,6 +196,8 @@ def _parse_intrusion_sets(objects: list) -> list[dict]:
|
||||
"references": references[:20], # cap to avoid bloat
|
||||
"first_seen": obj.get("first_seen"),
|
||||
"last_seen": obj.get("last_seen"),
|
||||
"motivation": motivation,
|
||||
"sophistication": sophistication,
|
||||
})
|
||||
|
||||
logger.info("Parsed %d intrusion-sets (threat actors)", len(actors))
|
||||
@@ -275,6 +314,11 @@ def sync(db: Session) -> dict:
|
||||
db_actor.references = actor_dict["references"]
|
||||
db_actor.first_seen = actor_dict.get("first_seen")
|
||||
db_actor.last_seen = actor_dict.get("last_seen")
|
||||
# Update enrichment fields if available
|
||||
if actor_dict.get("motivation"):
|
||||
db_actor.motivation = actor_dict["motivation"]
|
||||
if actor_dict.get("sophistication"):
|
||||
db_actor.sophistication = actor_dict["sophistication"]
|
||||
stix_to_db_actor[stix_id] = db_actor
|
||||
actors_skipped += 1
|
||||
else:
|
||||
@@ -288,6 +332,8 @@ def sync(db: Session) -> dict:
|
||||
references=actor_dict["references"],
|
||||
first_seen=actor_dict.get("first_seen"),
|
||||
last_seen=actor_dict.get("last_seen"),
|
||||
motivation=actor_dict.get("motivation"),
|
||||
sophistication=actor_dict.get("sophistication"),
|
||||
is_active=True,
|
||||
)
|
||||
db.add(db_actor)
|
||||
|
||||
Reference in New Issue
Block a user