feat(backend): enforce campaign scheduling, add reviewer queue filter, tactic order, duplicate-test counts
- start_execution now blocks a test from starting before its campaign's start_date, closing a gap where the field was persisted but never enforced - GET /tests gains a reviewer_id "my reviews" filter for the red_review/ blue_review lead-gate stage, distinct from pending_validation_side (which only ever covered the later in_review stage and ignored assignment entirely) - get_coverage_by_tactic now returns all 14 MITRE tactics in canonical kill-chain order instead of an alphabetical, partial list — the regular Dashboard and Executive Dashboard both consume this endpoint and previously disagreed on order/completeness - test-template listing now includes existing_test_count per technique so the catalog can warn before creating a likely-duplicate test
This commit is contained in:
@@ -38,6 +38,26 @@ from app.schemas.metrics import (
|
||||
ValidationRate,
|
||||
)
|
||||
|
||||
# Canonical MITRE ATT&CK kill-chain order — used to sort "coverage by
|
||||
# tactic" consistently everywhere it's rendered (dashboard table, executive
|
||||
# bar chart), instead of each consumer inventing its own order/subset.
|
||||
MITRE_TACTIC_ORDER: list[str] = [
|
||||
"reconnaissance",
|
||||
"resource-development",
|
||||
"initial-access",
|
||||
"execution",
|
||||
"persistence",
|
||||
"privilege-escalation",
|
||||
"defense-evasion",
|
||||
"credential-access",
|
||||
"discovery",
|
||||
"lateral-movement",
|
||||
"collection",
|
||||
"command-and-control",
|
||||
"exfiltration",
|
||||
"impact",
|
||||
]
|
||||
|
||||
|
||||
# Define function get_coverage_summary
|
||||
def get_coverage_summary(db: Session) -> CoverageSummary:
|
||||
@@ -112,6 +132,12 @@ def get_coverage_by_tactic(db: Session) -> list[TacticCoverage]:
|
||||
lambda: {s.value: 0 for s in TechniqueStatus}
|
||||
)
|
||||
|
||||
# Materialize every canonical tactic up front so the response always
|
||||
# includes all 14, zero-filled, rather than only whichever tactics
|
||||
# happen to have techniques seeded.
|
||||
for tactic in MITRE_TACTIC_ORDER:
|
||||
tactic_data[tactic] # noqa: B018 — touch to create the defaultdict entry
|
||||
|
||||
# Iterate over techniques
|
||||
for tactic_str, status in techniques:
|
||||
# Check: not tactic_str
|
||||
@@ -128,10 +154,17 @@ def get_coverage_by_tactic(db: Session) -> list[TacticCoverage]:
|
||||
# Assign tactic_data[tactic][status.value] = 1
|
||||
tactic_data[tactic][status.value] += 1
|
||||
|
||||
def _tactic_sort_key(tactic: str) -> tuple[int, str]:
|
||||
try:
|
||||
return (MITRE_TACTIC_ORDER.index(tactic), tactic)
|
||||
except ValueError:
|
||||
# Anything outside the canonical 14 (e.g. "unknown") sorts last.
|
||||
return (len(MITRE_TACTIC_ORDER), tactic)
|
||||
|
||||
# Assign result = []
|
||||
result = []
|
||||
# Iterate over sorted(tactic_data)
|
||||
for tactic in sorted(tactic_data):
|
||||
for tactic in sorted(tactic_data, key=_tactic_sort_key):
|
||||
# Assign counts = tactic_data[tactic]
|
||||
counts = tactic_data[tactic]
|
||||
# Assign total = sum(counts.values())
|
||||
|
||||
Reference in New Issue
Block a user