feat(evaluations): bulk approve evaluation tests with 4-step confirmation modal
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend: - POST /system/attck-evaluations/bulk-approve: finds all [EVAL R*] tests in in_review state, approves blue side, transitions to validated, recalculates technique statuses, audit logs each test - GET /system/attck-evaluations/pending-count: returns count of pending eval tests Frontend: - BulkApproveModal: 4 mandatory checkboxes before confirm button enables (lab env / not org detection / metrics impact / spot-check recommendation) - Bulk Approve button in header badge showing pending count - Green result banner showing approved tests + techniques recalculated - Invalidates techniques, metrics and review-queue queries on success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -632,6 +632,121 @@ def check_new_evaluation_round(
|
||||
return check_for_new_round(db)
|
||||
|
||||
|
||||
@router.post("/attck-evaluations/bulk-approve")
|
||||
def bulk_approve_evaluation_tests(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Bulk-approve all Blue Team validation for ATT&CK Evaluation imported tests.
|
||||
|
||||
Finds every test in ``in_review`` state whose name starts with ``[EVAL R``
|
||||
and approves the Blue Team side. Because all evaluation imports pre-approve
|
||||
the Red Team side, this moves every matched test to ``validated`` state.
|
||||
|
||||
**Important caveats** (enforced by UI warnings before this is called):
|
||||
- Results come from a controlled MITRE lab, NOT the organisation's env.
|
||||
- Validated tests will immediately affect coverage metrics and dashboards.
|
||||
- Blue Leads should still spot-check high-priority techniques individually.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from app.models.test import Test
|
||||
from app.models.enums import TestState
|
||||
from app.models.technique import Technique
|
||||
from app.services.status_service import recalculate_technique_status
|
||||
from app.services.audit_service import log_action
|
||||
|
||||
# Find all pending evaluation tests
|
||||
pending = (
|
||||
db.query(Test)
|
||||
.filter(
|
||||
Test.state == TestState.in_review,
|
||||
Test.name.like("[EVAL R%"),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not pending:
|
||||
return {
|
||||
"approved": 0,
|
||||
"techniques_recalculated": 0,
|
||||
"message": "No pending evaluation tests found — nothing to approve.",
|
||||
}
|
||||
|
||||
now = datetime.utcnow()
|
||||
affected_technique_ids: set = set()
|
||||
|
||||
for test in pending:
|
||||
# Approve blue side
|
||||
test.blue_validation_status = "approved"
|
||||
test.blue_validated_by = current_user.id
|
||||
test.blue_validated_at = now
|
||||
test.blue_validation_notes = (
|
||||
"Bulk-approved via ATT&CK Evaluations admin panel. "
|
||||
"Source: MITRE lab environment — not organisational detection."
|
||||
)
|
||||
|
||||
# Red side was pre-approved during import → move to validated
|
||||
if test.red_validation_status == "approved":
|
||||
test.state = TestState.validated
|
||||
# else stays in_review (shouldn't happen for eval imports, but be safe)
|
||||
|
||||
if test.technique_id:
|
||||
affected_technique_ids.add(test.technique_id)
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
action="bulk_eval_approve",
|
||||
entity_type="test",
|
||||
entity_id=test.id,
|
||||
details={"source": "attck_evaluation_bulk_approve"},
|
||||
)
|
||||
|
||||
db.flush()
|
||||
|
||||
# Recalculate coverage for every touched technique
|
||||
for tech_id in affected_technique_ids:
|
||||
tech = db.query(Technique).filter(Technique.id == tech_id).first()
|
||||
if tech:
|
||||
recalculate_technique_status(db, tech)
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
"Bulk eval approval: %d tests validated, %d techniques recalculated (by %s)",
|
||||
len(pending), len(affected_technique_ids), current_user.email,
|
||||
)
|
||||
|
||||
return {
|
||||
"approved": len(pending),
|
||||
"techniques_recalculated": len(affected_technique_ids),
|
||||
"message": (
|
||||
f"{len(pending)} evaluation tests approved and moved to Validated. "
|
||||
f"{len(affected_technique_ids)} technique statuses recalculated."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/attck-evaluations/pending-count")
|
||||
def get_pending_evaluation_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Return the number of imported evaluation tests still awaiting Blue approval."""
|
||||
from app.models.test import Test
|
||||
from app.models.enums import TestState
|
||||
|
||||
count = (
|
||||
db.query(Test)
|
||||
.filter(
|
||||
Test.state == TestState.in_review,
|
||||
Test.name.like("[EVAL R%"),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
return {"pending": count}
|
||||
|
||||
|
||||
@router.post("/email-test")
|
||||
def send_test_email(
|
||||
payload: EmailTestRequest,
|
||||
|
||||
Reference in New Issue
Block a user