Files
Aegis/backend/tests/test_procedure_suggestions.py
T
kitos 8fcd733d4a
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
fix(tests): merge approved procedure suggestions instead of overwriting
Approving a suggestion was replacing the template's whole procedure
field with just that submission's extracted text, silently dropping
whatever an earlier approved round had already added. Now it appends
only the genuinely new lines, so templates accumulate commands across
rounds instead of losing them.
2026-07-14 16:25:24 +02:00

275 lines
11 KiB
Python

"""End-to-end coverage for the procedure-suggestion review workflow.
An operator submitting a round with a filled-in procedure field that
contains an extractable command should produce a pending
ProcedureSuggestion against the originating template; a lead can then
approve it (writing it into the template) or reject it (leaving the
template untouched). Nothing is ever written to a template automatically.
"""
import uuid
import pytest
from app.models.enums import TeamSide
from app.models.evidence import Evidence
def _add_evidence(db, test_id, team: TeamSide):
ev = Evidence(
test_id=uuid.UUID(test_id),
file_name="proof.txt",
file_path="s3://bucket/proof.txt",
sha256_hash="a" * 64,
team=team,
)
db.add(ev)
db.commit()
@pytest.fixture
def technique(api, auth_headers):
resp = api(
"post", "/api/v1/techniques", auth_headers,
json={"mitre_id": "T1059.200", "name": "Command Line Suggestions"},
)
assert resp.status_code == 201, resp.text
return resp.json()["id"]
@pytest.fixture
def template(api, red_lead_headers, technique):
resp = api(
"post", "/api/v1/test-templates", red_lead_headers,
json={
"mitre_technique_id": "T1059.200",
"name": "Suggestion source template",
"attack_procedure": "Run a discovery command on the target.",
"detect_suggested_procedure": "Check process creation logs.",
},
)
assert resp.status_code == 201, resp.text
return resp.json()
@pytest.fixture
def test_from_template(api, red_lead_headers, technique, template):
resp = api(
"post", "/api/v1/tests/from-template", red_lead_headers,
json={"template_id": template["id"], "technique_id": technique},
)
assert resp.status_code == 201, resp.text
return resp.json()["id"]
def test_red_submission_with_command_creates_pending_suggestion(
client, db, api, test_from_template, red_tech_headers, red_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nGot back SeDebugPrivilege."},
)
_add_evidence(db, test_id, TeamSide.red)
resp = api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
assert resp.status_code == 200, resp.text
listed = api("get", "/api/v1/procedure-suggestions", red_lead_headers)
assert listed.status_code == 200, listed.text
suggestions = listed.json()
assert len(suggestions) == 1
assert suggestions[0]["team"] == "red"
assert suggestions[0]["suggested_text"] == "whoami /priv"
assert suggestions[0]["status"] == "pending"
def test_blue_submission_with_command_creates_pending_suggestion(
client, db, api, test_from_template, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
api("post", f"/api/v1/tests/{test_id}/review-red", red_lead_headers, json={"decision": "approve"})
api("post", f"/api/v1/tests/{test_id}/start-blue-work", blue_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/blue", blue_tech_headers,
json={
"detection_result": "detected",
"detect_procedure": "Checked logs.\nGet-WinEvent -LogName Security | where Id -eq 4688\nFound it.",
},
)
_add_evidence(db, test_id, TeamSide.blue)
resp = api("post", f"/api/v1/tests/{test_id}/submit-blue", blue_tech_headers)
assert resp.status_code == 200, resp.text
listed = api("get", "/api/v1/procedure-suggestions", blue_lead_headers)
assert listed.status_code == 200, listed.text
suggestions = listed.json()
assert len(suggestions) == 1
assert suggestions[0]["team"] == "blue"
assert "Get-WinEvent" in suggestions[0]["suggested_text"]
def test_submission_without_extractable_command_creates_no_suggestion(
client, db, api, test_from_template, red_tech_headers, red_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Just talked to the target and it agreed to be compromised."},
)
_add_evidence(db, test_id, TeamSide.red)
resp = api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
assert resp.status_code == 200, resp.text
listed = api("get", "/api/v1/procedure-suggestions", red_lead_headers)
assert listed.json() == []
def test_approving_a_suggestion_writes_it_into_the_template(
client, db, api, test_from_template, template, red_tech_headers, red_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
suggestion = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()[0]
approve = api(
"post", f"/api/v1/procedure-suggestions/{suggestion['id']}/approve", red_lead_headers,
)
assert approve.status_code == 200, approve.text
assert approve.json()["status"] == "approved"
reloaded = api("get", f"/api/v1/test-templates/{template['id']}", red_lead_headers)
assert reloaded.json()["attack_procedure"] == "Run a discovery command on the target.\nwhoami /priv"
listed = api("get", "/api/v1/procedure-suggestions", red_lead_headers)
assert listed.json() == []
def test_rejecting_a_suggestion_leaves_the_template_untouched(
client, db, api, test_from_template, template, red_tech_headers, red_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
suggestion = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()[0]
reject = api(
"post", f"/api/v1/procedure-suggestions/{suggestion['id']}/reject", red_lead_headers,
)
assert reject.status_code == 200, reject.text
assert reject.json()["status"] == "rejected"
reloaded = api("get", f"/api/v1/test-templates/{template['id']}", red_lead_headers)
assert reloaded.json()["attack_procedure"] == "Run a discovery command on the target."
def test_approving_a_later_suggestion_does_not_erase_an_earlier_approved_one(
client, db, api, test_from_template, template, red_tech_headers, red_lead_headers,
):
"""Two separate rounds, each contributing a different command, must
both survive in the template once their suggestions are approved —
approving round 2's suggestion must not wipe out what round 1 added."""
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
first_suggestion = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()[0]
api("post", f"/api/v1/procedure-suggestions/{first_suggestion['id']}/approve", red_lead_headers)
api(
"post", f"/api/v1/tests/{test_id}/review-red", red_lead_headers,
json={"decision": "reopen", "notes": "one more pass"},
)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran a follow-up check.\nnltest /dsgetdc:corp\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
second_suggestion = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()[0]
assert second_suggestion["suggested_text"] == "nltest /dsgetdc:corp"
approve_second = api(
"post", f"/api/v1/procedure-suggestions/{second_suggestion['id']}/approve", red_lead_headers,
)
assert approve_second.status_code == 200, approve_second.text
reloaded = api("get", f"/api/v1/test-templates/{template['id']}", red_lead_headers)
assert reloaded.json()["attack_procedure"] == (
"Run a discovery command on the target.\nwhoami /priv\nnltest /dsgetdc:corp"
)
def test_blue_lead_cannot_review_a_red_suggestion(
client, db, api, test_from_template, red_tech_headers, red_lead_headers, blue_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
suggestion = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()[0]
# A blue_lead sees no red suggestions in their own queue...
blue_view = api("get", "/api/v1/procedure-suggestions", blue_lead_headers)
assert blue_view.json() == []
# ...and is forbidden from acting on one directly by id.
forbidden = api(
"post", f"/api/v1/procedure-suggestions/{suggestion['id']}/approve", blue_lead_headers,
)
assert forbidden.status_code == 403
def test_duplicate_submission_does_not_create_a_second_pending_suggestion(
client, db, api, test_from_template, red_tech_headers, red_lead_headers,
):
test_id = test_from_template
api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery.\nwhoami /priv\nDone."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
api(
"post", f"/api/v1/tests/{test_id}/review-red", red_lead_headers,
json={"decision": "reopen", "notes": "please redo this round"},
)
api(
"patch", f"/api/v1/tests/{test_id}/red", red_tech_headers,
json={"procedure_text": "Ran discovery again.\nwhoami /priv\nStill works."},
)
_add_evidence(db, test_id, TeamSide.red)
api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers)
listed = api("get", "/api/v1/procedure-suggestions", red_lead_headers).json()
assert len(listed) == 1