feat: sprint 4 — UI polish + dark mode + workflow tightening + process hygiene #7

Merged
knacky merged 15 commits from sprint/4-ui-polish into main 2026-05-28 04:01:21 +00:00
2 changed files with 27 additions and 13 deletions
Showing only changes of commit a824df06b2 - Show all commits

View File

@@ -42,6 +42,9 @@ _TACTIC_IDS: dict[str, str] = {
"TA0040": "impact", "TA0040": "impact",
} }
# Reverse: slug → TA-id (derived from _TACTIC_IDS, used by _build_matrix).
_SLUG_TO_TA_ID: dict[str, str] = {v: k for k, v in _TACTIC_IDS.items()}
TACTIC_NAMES: dict[str, str] = { TACTIC_NAMES: dict[str, str] = {
"initial-access": "Initial Access", "initial-access": "Initial Access",
"execution": "Execution", "execution": "Execution",
@@ -114,14 +117,16 @@ def _build_matrix(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
subs.sort(key=lambda x: x["name"]) subs.sort(key=lambda x: x["name"])
matrix: list[dict[str, Any]] = [] matrix: list[dict[str, Any]] = []
for tactic_id in _TACTIC_ORDER: for slug in _TACTIC_ORDER:
techs = tactic_techs.get(tactic_id, []) techs = tactic_techs.get(slug, [])
# Sort techniques alphabetically. # Sort techniques alphabetically.
techs_sorted = sorted(techs, key=lambda x: x["name"]) techs_sorted = sorted(techs, key=lambda x: x["name"])
tactic_name = TACTIC_NAMES.get(tactic_id, tactic_id.replace("-", " ").title()) tactic_name = TACTIC_NAMES.get(slug, slug.replace("-", " ").title())
# Expose TA-id so the frontend can send tactic_ids back in PATCH unchanged.
ta_id = _SLUG_TO_TA_ID.get(slug, slug)
matrix.append( matrix.append(
{ {
"tactic_id": tactic_id, "tactic_id": ta_id,
"tactic_name": tactic_name, "tactic_name": tactic_name,
"techniques": [ "techniques": [
{ {

View File

@@ -305,14 +305,14 @@ def test_get_matrix_returns_ordered_tactics(bundle_file: pathlib.Path) -> None:
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
tactic_ids = [t["tactic_id"] for t in matrix] tactic_ids = [t["tactic_id"] for t in matrix]
# initial-access must come before execution in canonical order. # TA0001 (initial-access) must come before TA0002 (execution) in canonical order.
assert tactic_ids.index("initial-access") < tactic_ids.index("execution") assert tactic_ids.index("TA0001") < tactic_ids.index("TA0002")
def test_get_matrix_subtechniques_nested(bundle_file: pathlib.Path) -> None: def test_get_matrix_subtechniques_nested(bundle_file: pathlib.Path) -> None:
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
exec_tactic = next(t for t in matrix if t["tactic_id"] == "execution") exec_tactic = next(t for t in matrix if t["tactic_id"] == "TA0002")
t1059 = next((t for t in exec_tactic["techniques"] if t["id"] == "T1059"), None) t1059 = next((t for t in exec_tactic["techniques"] if t["id"] == "T1059"), None)
assert t1059 is not None assert t1059 is not None
sub_ids = [s["id"] for s in t1059["subtechniques"]] sub_ids = [s["id"] for s in t1059["subtechniques"]]
@@ -323,7 +323,7 @@ def test_get_matrix_subtechniques_nested(bundle_file: pathlib.Path) -> None:
def test_get_matrix_subtechniques_sorted_by_name(bundle_file: pathlib.Path) -> None: def test_get_matrix_subtechniques_sorted_by_name(bundle_file: pathlib.Path) -> None:
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
exec_tactic = next(t for t in matrix if t["tactic_id"] == "execution") exec_tactic = next(t for t in matrix if t["tactic_id"] == "TA0002")
t1059 = next(t for t in exec_tactic["techniques"] if t["id"] == "T1059") t1059 = next(t for t in exec_tactic["techniques"] if t["id"] == "T1059")
names = [s["name"] for s in t1059["subtechniques"]] names = [s["name"] for s in t1059["subtechniques"]]
assert names == sorted(names) assert names == sorted(names)
@@ -332,7 +332,7 @@ def test_get_matrix_subtechniques_sorted_by_name(bundle_file: pathlib.Path) -> N
def test_get_matrix_techniques_sorted_by_name(bundle_file: pathlib.Path) -> None: def test_get_matrix_techniques_sorted_by_name(bundle_file: pathlib.Path) -> None:
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
ia_tactic = next(t for t in matrix if t["tactic_id"] == "initial-access") ia_tactic = next(t for t in matrix if t["tactic_id"] == "TA0001")
names = [t["name"] for t in ia_tactic["techniques"]] names = [t["name"] for t in ia_tactic["techniques"]]
assert names == sorted(names) assert names == sorted(names)
@@ -340,7 +340,7 @@ def test_get_matrix_techniques_sorted_by_name(bundle_file: pathlib.Path) -> None
def test_get_matrix_technique_no_subtechniques(bundle_file: pathlib.Path) -> None: def test_get_matrix_technique_no_subtechniques(bundle_file: pathlib.Path) -> None:
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
ia_tactic = next(t for t in matrix if t["tactic_id"] == "initial-access") ia_tactic = next(t for t in matrix if t["tactic_id"] == "TA0001")
phishing = next((t for t in ia_tactic["techniques"] if t["id"] == "T1566"), None) phishing = next((t for t in ia_tactic["techniques"] if t["id"] == "T1566"), None)
assert phishing is not None assert phishing is not None
assert phishing["subtechniques"] == [] assert phishing["subtechniques"] == []
@@ -355,8 +355,8 @@ def test_matrix_endpoint_ok(
data = resp.get_json() data = resp.get_json()
assert isinstance(data, list) assert isinstance(data, list)
tactic_ids = [t["tactic_id"] for t in data] tactic_ids = [t["tactic_id"] for t in data]
assert "initial-access" in tactic_ids assert "TA0001" in tactic_ids # initial-access
assert "execution" in tactic_ids assert "TA0002" in tactic_ids # execution
def test_matrix_endpoint_503_when_not_loaded( def test_matrix_endpoint_503_when_not_loaded(
@@ -389,6 +389,15 @@ def test_get_matrix_command_and_control_display_name(bundle_file: pathlib.Path)
"""MITRE official name uses lowercase 'and' — not title-cased.""" """MITRE official name uses lowercase 'and' — not title-cased."""
mitre_svc.load_bundle(bundle_file) mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix() matrix = mitre_svc.get_matrix()
c2 = next((t for t in matrix if t["tactic_id"] == "command-and-control"), None) c2 = next((t for t in matrix if t["tactic_id"] == "TA0011"), None)
assert c2 is not None assert c2 is not None
assert c2["tactic_name"] == "Command and Control" assert c2["tactic_name"] == "Command and Control"
def test_get_matrix_tactic_id_is_ta_format(bundle_file: pathlib.Path) -> None:
"""Matrix tactic_id must use TA-format so frontend can send it back in PATCH tactic_ids."""
mitre_svc.load_bundle(bundle_file)
matrix = mitre_svc.get_matrix()
for entry in matrix:
tid = entry["tactic_id"]
assert tid.startswith("TA"), f"tactic_id {tid!r} must be TA-format, not a slug"