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:
@@ -146,6 +146,15 @@ class TTPTag(SQLModel, table=True):
|
||||
# ID cannot render deterministically in MITRE Navigator.
|
||||
attack_release: str = Field(index=True)
|
||||
|
||||
# Canonical attack.mitre.org URL for this technique (or
|
||||
# sub-technique when present). Resolved at insert via
|
||||
# decnet.ttp.attack_stix.mitre_url_for from the loaded STIX
|
||||
# bundle. Nullable because (a) the bundle may not be loaded in
|
||||
# certain test paths and (b) a future release could deprecate
|
||||
# a technique we have legacy tags for. Not indexed — derived
|
||||
# deeplink, not a query target; technique_id is already indexed.
|
||||
mitre_url: Optional[str] = Field(default=None)
|
||||
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
index=True,
|
||||
@@ -256,6 +265,7 @@ class TechniqueRollupRow(BaseModel):
|
||||
tactic: str
|
||||
count: int
|
||||
last_seen: datetime
|
||||
mitre_url: Optional[str] = None
|
||||
|
||||
|
||||
class IdentityTechniqueRow(BaseModel):
|
||||
@@ -278,6 +288,7 @@ class IdentityTechniqueRow(BaseModel):
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
confidence_max: float
|
||||
mitre_url: Optional[str] = None
|
||||
|
||||
|
||||
class TTPTagDetailRow(BaseModel):
|
||||
@@ -306,6 +317,7 @@ class TTPTagDetailRow(BaseModel):
|
||||
evidence: dict[str, Any] = Field(default_factory=dict)
|
||||
attack_release: str
|
||||
created_at: datetime
|
||||
mitre_url: Optional[str] = None
|
||||
|
||||
|
||||
class CampaignTechniqueRow(BaseModel):
|
||||
@@ -320,6 +332,7 @@ class CampaignTechniqueRow(BaseModel):
|
||||
count: int
|
||||
identity_count: int
|
||||
last_seen: datetime
|
||||
mitre_url: Optional[str] = None
|
||||
|
||||
|
||||
class RuleCatalogueRow(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user