feat: sprint 4 — UI polish + dark mode + workflow tightening + process hygiene #7
@@ -42,6 +42,9 @@ _TACTIC_IDS: dict[str, str] = {
|
||||
"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] = {
|
||||
"initial-access": "Initial Access",
|
||||
"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"])
|
||||
|
||||
matrix: list[dict[str, Any]] = []
|
||||
for tactic_id in _TACTIC_ORDER:
|
||||
techs = tactic_techs.get(tactic_id, [])
|
||||
for slug in _TACTIC_ORDER:
|
||||
techs = tactic_techs.get(slug, [])
|
||||
# Sort techniques alphabetically.
|
||||
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(
|
||||
{
|
||||
"tactic_id": tactic_id,
|
||||
"tactic_id": ta_id,
|
||||
"tactic_name": tactic_name,
|
||||
"techniques": [
|
||||
{
|
||||
|
||||
@@ -305,14 +305,14 @@ def test_get_matrix_returns_ordered_tactics(bundle_file: pathlib.Path) -> None:
|
||||
mitre_svc.load_bundle(bundle_file)
|
||||
matrix = mitre_svc.get_matrix()
|
||||
tactic_ids = [t["tactic_id"] for t in matrix]
|
||||
# initial-access must come before execution in canonical order.
|
||||
assert tactic_ids.index("initial-access") < tactic_ids.index("execution")
|
||||
# TA0001 (initial-access) must come before TA0002 (execution) in canonical order.
|
||||
assert tactic_ids.index("TA0001") < tactic_ids.index("TA0002")
|
||||
|
||||
|
||||
def test_get_matrix_subtechniques_nested(bundle_file: pathlib.Path) -> None:
|
||||
mitre_svc.load_bundle(bundle_file)
|
||||
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)
|
||||
assert t1059 is not None
|
||||
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:
|
||||
mitre_svc.load_bundle(bundle_file)
|
||||
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")
|
||||
names = [s["name"] for s in t1059["subtechniques"]]
|
||||
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:
|
||||
mitre_svc.load_bundle(bundle_file)
|
||||
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"]]
|
||||
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:
|
||||
mitre_svc.load_bundle(bundle_file)
|
||||
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)
|
||||
assert phishing is not None
|
||||
assert phishing["subtechniques"] == []
|
||||
@@ -355,8 +355,8 @@ def test_matrix_endpoint_ok(
|
||||
data = resp.get_json()
|
||||
assert isinstance(data, list)
|
||||
tactic_ids = [t["tactic_id"] for t in data]
|
||||
assert "initial-access" in tactic_ids
|
||||
assert "execution" in tactic_ids
|
||||
assert "TA0001" in tactic_ids # initial-access
|
||||
assert "TA0002" in tactic_ids # execution
|
||||
|
||||
|
||||
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_svc.load_bundle(bundle_file)
|
||||
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["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"
|
||||
|
||||
Reference in New Issue
Block a user