feat(ttp): promote mitre_url to first-class TTPTag column + propagate everywhere
Phase 2 attached mitre_url to intel-emitted tags' evidence JSON; Phase 3 promotes it to a real column populated for *every* tag — intel, credential, behavioral, canary, identity, email, rule-engine — from one source. Pre-v1, so the SQLModel field is added directly without an Alembic migration. - TTPTag gains mitre_url: Optional[str] (not indexed — derived deeplink, not a query target; technique_id is already indexed). - _emit.py and rule_engine._evaluate_rules both populate mitre_url via attack_stix.mitre_url_for(sub_technique_id or technique_id). Sub-technique URL when present, else parent. The two construction sites stay separate because the rule_engine path carries per-emit span instrumentation that emit_tags() can't preserve without threading a span object through; minimal-change beats forced refactor here. - intel_lifter strips mitre_url from evidence_extra in all four decision functions. The column is canonical now; duplicating in the JSON column would drift when the bundle moves. The unused TechniqueEmission import + tracking dicts removed too. - IdentityTechniqueRow / TechniqueRollupRow / TTPTagDetailRow / CampaignTechniqueRow gain mitre_url: Optional[str]. - sqlmodel_repo/ttp.py:_mitre_url_for added; the 5 row-builder sites pass mitre_url=_mitre_url_for(sub_technique_id or technique_id) alongside the existing technique_name resolution. - api_get_tag_details.py needs no change — list_tags_by_scope_and _technique already returns model_dump() rows that flow the new column through **row spread to TTPTagDetailRow. - tests/ttp/test_emit_attaches_mitre_url.py covers both construction paths (top-level, sub-tech, unknown, multi-emit) and a regression test that intel_lifter evidence dicts no longer contain mitre_url.
This commit is contained in:
@@ -21,6 +21,7 @@ from sqlalchemy import func, select
|
||||
from sqlmodel import col
|
||||
|
||||
from decnet.ttp.attack_catalog import technique_name as _technique_name
|
||||
from decnet.ttp.attack_stix import mitre_url_for as _mitre_url_for
|
||||
from decnet.web.db.models import (
|
||||
Attacker,
|
||||
AttackerIdentity,
|
||||
@@ -117,6 +118,7 @@ class TTPMixin(_MixinBase):
|
||||
technique_name=_technique_name(r.technique_id),
|
||||
sub_technique_id=r.sub_technique_id,
|
||||
sub_technique_name=_technique_name(r.sub_technique_id),
|
||||
mitre_url=_mitre_url_for(r.sub_technique_id or r.technique_id),
|
||||
tactic=r.tactic,
|
||||
count=r.count,
|
||||
first_seen=r.first_seen,
|
||||
@@ -155,6 +157,7 @@ class TTPMixin(_MixinBase):
|
||||
technique_name=_technique_name(r.technique_id),
|
||||
sub_technique_id=r.sub_technique_id,
|
||||
sub_technique_name=_technique_name(r.sub_technique_id),
|
||||
mitre_url=_mitre_url_for(r.sub_technique_id or r.technique_id),
|
||||
tactic=r.tactic,
|
||||
count=r.count,
|
||||
first_seen=r.first_seen,
|
||||
@@ -199,6 +202,7 @@ class TTPMixin(_MixinBase):
|
||||
technique_name=_technique_name(r.technique_id),
|
||||
sub_technique_id=r.sub_technique_id,
|
||||
sub_technique_name=_technique_name(r.sub_technique_id),
|
||||
mitre_url=_mitre_url_for(r.sub_technique_id or r.technique_id),
|
||||
tactic=r.tactic,
|
||||
count=r.count,
|
||||
identity_count=r.identity_count,
|
||||
@@ -235,6 +239,7 @@ class TTPMixin(_MixinBase):
|
||||
technique_name=_technique_name(r.technique_id),
|
||||
sub_technique_id=r.sub_technique_id,
|
||||
sub_technique_name=_technique_name(r.sub_technique_id),
|
||||
mitre_url=_mitre_url_for(r.sub_technique_id or r.technique_id),
|
||||
tactic=r.tactic,
|
||||
count=r.count,
|
||||
first_seen=r.first_seen,
|
||||
@@ -408,6 +413,7 @@ class TTPMixin(_MixinBase):
|
||||
technique_name=_technique_name(r.technique_id),
|
||||
sub_technique_id=r.sub_technique_id,
|
||||
sub_technique_name=_technique_name(r.sub_technique_id),
|
||||
mitre_url=_mitre_url_for(r.sub_technique_id or r.technique_id),
|
||||
tactic=r.tactic,
|
||||
count=r.count,
|
||||
last_seen=r.last_seen,
|
||||
|
||||
Reference in New Issue
Block a user