fix(threat-actors): fix 500 on search + populate motivation from STIX
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:
kitos
2026-05-29 14:09:04 +02:00
parent 7d856bef43
commit e49eca0b24
2 changed files with 49 additions and 3 deletions

View File

@@ -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)