feat(ttp): add mitre_url_for + groups_using_technique helpers

Two reusable bundle-derived lookups that the next two commits build
on:

- mitre_url_for(tid) returns the canonical attack.mitre.org URL by
  reading external_references on the cached attack-pattern. Backed
  by the existing lru-cached _attack_pattern_by_id so per-call cost
  is constant. Handles top-level techniques and sub-techniques
  (T1059.004 -> .../techniques/T1059/004).
- GroupRef + groups_using_technique(tid) surface the intrusion-set
  reverse index from the loaded bundle: given a technique, return
  the MITRE-tracked groups documented as using it. Sorted by
  group_id for deterministic responses; lru-cached. Sub-technique
  semantics match ATT&CK Navigator (do NOT auto-union with parent).
- decnet/ttp/data/intel_loader._mitre_url_for collapses to a thin
  re-export of attack_stix.mitre_url_for; the loader keeps mitre_url
  on TechniqueEmission for the eventual STIX export.
- tests/ttp/test_attack_url.py covers both helpers: top-level + sub
  URLs, unknown -> None / (), GroupRef immutability + hashability,
  deterministic ordering, sub-technique distinct from parent.
This commit is contained in:
2026-05-09 06:32:04 -04:00
parent 0a9a2f9021
commit e50474cb66
3 changed files with 212 additions and 7 deletions

View File

@@ -145,13 +145,13 @@ class ProviderMapping:
def _mitre_url_for(technique_id: str) -> str | None:
obj = attack_stix._attack_pattern_by_id(technique_id)
if obj is None:
return None
for ref in obj.get("external_references", []):
if ref.get("source_name") == "mitre-attack":
return ref.get("url")
return None
"""Compatibility shim — collapsed to a re-export of :func:`attack_stix.mitre_url_for`.
Public callers should import :func:`decnet.ttp.attack_stix.mitre_url_for`
directly. Kept here so the in-tree loader stays self-contained
when someone reads it cold.
"""
return attack_stix.mitre_url_for(technique_id)
def _data_path(provider: str) -> Path: