feat(ttp): show canonical ATT&CK technique names in the TTPs UI

"T1595" alone is opaque; "T1595 — Active Scanning" tells you the
story at a glance. The names come from a backend-side static catalogue
pinned to the same ATT&CK release as the rule engine
(_ATTACK_RELEASE = "v15.1") — names are the canonical MITRE labels,
not author-supplied strings on rules, so a rule author can't typo a
name and the entire fleet sees the typo.

- New `decnet/ttp/attack_catalog.py` with `TECHNIQUE_NAMES` covering
  every technique_id + sub_technique_id emitted by `rules/ttp/`
  (R0001..R0058 → 69 IDs in the v0 pack).
- `IdentityTechniqueRow` / `TechniqueRollupRow` / `CampaignTechniqueRow`
  / `TTPTagDetailRow` gain optional `technique_name` /
  `sub_technique_name` fields. Repo + router populate them from the
  catalogue at row-construction time. None when an ID isn't in the
  catalogue — UI falls back to the bare ID.
- Coverage test (`tests/ttp/test_attack_catalog.py`) walks every
  YAML rule and asserts every emitted ID has a catalogue entry, so
  a future rule author who forgets to update the catalogue gets a
  loud failure rather than a silent UI fallback.

Frontend:
- `TTPsObservedSection` shows "T1595.002 — Active Scanning:
  Vulnerability Scanning" instead of just the ID, with overflow
  ellipsis + tooltip for narrow viewports. Inspector header /
  TECHNIQUE row also surface the names.
This commit is contained in:
2026-05-02 03:10:07 -04:00
parent 42e9492118
commit 84699f89da
7 changed files with 247 additions and 10 deletions

View File

@@ -20,6 +20,7 @@ from typing import Any
from sqlalchemy import func, select
from sqlmodel import col
from decnet.ttp.attack_catalog import technique_name as _technique_name
from decnet.web.db.models import (
Attacker,
AttackerIdentity,
@@ -113,7 +114,9 @@ class TTPMixin(_MixinBase):
return [
IdentityTechniqueRow(
technique_id=r.technique_id,
technique_name=_technique_name(r.technique_id),
sub_technique_id=r.sub_technique_id,
sub_technique_name=_technique_name(r.sub_technique_id),
tactic=r.tactic,
count=r.count,
first_seen=r.first_seen,
@@ -149,7 +152,9 @@ class TTPMixin(_MixinBase):
return [
IdentityTechniqueRow(
technique_id=r.technique_id,
technique_name=_technique_name(r.technique_id),
sub_technique_id=r.sub_technique_id,
sub_technique_name=_technique_name(r.sub_technique_id),
tactic=r.tactic,
count=r.count,
first_seen=r.first_seen,
@@ -191,7 +196,9 @@ class TTPMixin(_MixinBase):
return [
CampaignTechniqueRow(
technique_id=r.technique_id,
technique_name=_technique_name(r.technique_id),
sub_technique_id=r.sub_technique_id,
sub_technique_name=_technique_name(r.sub_technique_id),
tactic=r.tactic,
count=r.count,
identity_count=r.identity_count,
@@ -225,7 +232,9 @@ class TTPMixin(_MixinBase):
return [
IdentityTechniqueRow(
technique_id=r.technique_id,
technique_name=_technique_name(r.technique_id),
sub_technique_id=r.sub_technique_id,
sub_technique_name=_technique_name(r.sub_technique_id),
tactic=r.tactic,
count=r.count,
first_seen=r.first_seen,
@@ -396,7 +405,9 @@ class TTPMixin(_MixinBase):
return [
TechniqueRollupRow(
technique_id=r.technique_id,
technique_name=_technique_name(r.technique_id),
sub_technique_id=r.sub_technique_id,
sub_technique_name=_technique_name(r.sub_technique_id),
tactic=r.tactic,
count=r.count,
last_seen=r.last_seen,