feat(phase-11): implement Red/Blue business logic services (T-106, T-107, T-108)
T-106: Create test_workflow_service.py with state-machine transitions for the complete test lifecycle (draft -> red_executing -> blue_evaluating -> in_review -> validated/rejected), dual validation by Red/Blue leads, and reopen capability with field cleanup. T-107: Update status_service.py to use detection_result from Blue Team instead of legacy result field, and differentiate between partial progress (some validated) vs all-in-progress states. T-108: Create atomic_import_service.py that downloads the Atomic Red Team repo as a ZIP (avoiding API rate limits), parses all atomics YAML files, and creates idempotent TestTemplate records mapped to MITRE techniques. Includes validation tests for all three tasks (19 checks total).
This commit is contained in:
@@ -128,5 +128,7 @@ def _evidence_to_out(evidence: Evidence) -> EvidenceOut:
|
||||
sha256_hash=evidence.sha256_hash,
|
||||
uploaded_by=evidence.uploaded_by,
|
||||
uploaded_at=evidence.uploaded_at,
|
||||
team=evidence.team,
|
||||
notes=evidence.notes,
|
||||
download_url=get_presigned_url(evidence.file_path),
|
||||
)
|
||||
|
||||
@@ -166,10 +166,11 @@ def validate_test(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
|
||||
):
|
||||
"""Mark a test as validated.
|
||||
"""Validate the red or blue side of a test (dual validation).
|
||||
|
||||
Sets ``state`` to *validated*, records ``validated_by`` / ``validated_at``,
|
||||
stores the ``result``, and recalculates the parent technique's global status.
|
||||
Red Lead approves/rejects the red side; Blue Lead approves/rejects the
|
||||
blue side. When *both* sides are approved the test state moves to
|
||||
``validated``. If either side is rejected the state moves to ``rejected``.
|
||||
"""
|
||||
test = (
|
||||
db.query(Test)
|
||||
@@ -184,10 +185,39 @@ def validate_test(
|
||||
detail="Test not found",
|
||||
)
|
||||
|
||||
test.state = TestState.validated
|
||||
now = datetime.utcnow()
|
||||
|
||||
if current_user.role in ("red_lead", "admin"):
|
||||
test.red_validation_status = payload.result.value
|
||||
test.red_validated_by = current_user.id
|
||||
test.red_validated_at = now
|
||||
side = "red"
|
||||
elif current_user.role == "blue_lead":
|
||||
test.blue_validation_status = payload.result.value
|
||||
test.blue_validated_by = current_user.id
|
||||
test.blue_validated_at = now
|
||||
side = "blue"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions to validate",
|
||||
)
|
||||
|
||||
# Store the overall result from the payload
|
||||
test.result = payload.result
|
||||
test.validated_by = current_user.id
|
||||
test.validated_at = datetime.utcnow()
|
||||
|
||||
# Determine aggregate state
|
||||
red_ok = test.red_validation_status == "approved"
|
||||
blue_ok = test.blue_validation_status == "approved"
|
||||
red_rej = test.red_validation_status == "rejected"
|
||||
blue_rej = test.blue_validation_status == "rejected"
|
||||
|
||||
if red_ok and blue_ok:
|
||||
test.state = TestState.validated
|
||||
elif red_rej or blue_rej:
|
||||
test.state = TestState.rejected
|
||||
else:
|
||||
test.state = TestState.in_review
|
||||
|
||||
db.commit()
|
||||
db.refresh(test)
|
||||
@@ -203,6 +233,7 @@ def validate_test(
|
||||
entity_type="test",
|
||||
entity_id=test.id,
|
||||
details={
|
||||
"side": side,
|
||||
"result": payload.result.value,
|
||||
"technique_id": str(test.technique_id),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user