mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
533 lines
21 KiB
Python
533 lines
21 KiB
Python
from __future__ import annotations
|
|
|
|
import concurrent.futures
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from plugins.violin_guard import bootstrap, command, hypotheses, ptt, state
|
|
from plugins.violin_guard.command import check_scope_authorization, validate_scope
|
|
from plugins.violin_guard.handlers.ptt_handlers import (
|
|
_redact_sensitive_note,
|
|
_start_ptt_task,
|
|
_validate_phase_exit,
|
|
)
|
|
from plugins.violin_guard.phases import Phase
|
|
|
|
|
|
def _scope(tmp_path: Path, targets: str, allowed: str = "recon") -> Path:
|
|
path = tmp_path / "scope.yaml"
|
|
path.write_text(
|
|
f"""targets:
|
|
{targets}
|
|
rules_of_engagement:
|
|
allowed_actions: [{allowed}]
|
|
forbidden_actions: []
|
|
engagement:
|
|
date: 2026-08-01
|
|
authorized_parties: [operator]
|
|
authorisation:
|
|
confirmed: true
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def test_domain_only_scope_is_valid(tmp_path: Path) -> None:
|
|
result = validate_scope(_scope(tmp_path, "domains: [app.example.test]"))
|
|
assert not any("ip_addresses" in error for error in result.errors)
|
|
assert not result.errors
|
|
|
|
|
|
def test_url_only_scope_is_valid(tmp_path: Path) -> None:
|
|
result = validate_scope(_scope(tmp_path, "urls: [https://app.example.test/login]"))
|
|
assert not result.errors
|
|
|
|
|
|
def test_exploitation_is_not_blocked_by_post_exploitation_forbidden_action() -> None:
|
|
result = check_scope_authorization(
|
|
{
|
|
"rules_of_engagement": {
|
|
"allowed_actions": ["exploitation"],
|
|
"forbidden_actions": ["post-exploitation"],
|
|
}
|
|
},
|
|
Phase.EXPLOITATION,
|
|
)
|
|
assert not result.errors
|
|
|
|
|
|
def test_scope_actions_reject_negations_and_containing_phrases() -> None:
|
|
for value in ("no exploitation", "post-exploitation", "pre-exploitation-check"):
|
|
result = check_scope_authorization(
|
|
{
|
|
"rules_of_engagement": {
|
|
"allowed_actions": [value],
|
|
"forbidden_actions": [],
|
|
}
|
|
},
|
|
Phase.EXPLOITATION,
|
|
)
|
|
assert result.errors, value
|
|
|
|
|
|
def test_credential_stuffing_does_not_match_hydra_by_substring() -> None:
|
|
result = check_scope_authorization(
|
|
{
|
|
"rules_of_engagement": {
|
|
"allowed_actions": ["recon"],
|
|
"forbidden_actions": ["credential-stuffing"],
|
|
}
|
|
},
|
|
Phase.RECON,
|
|
)
|
|
assert not any("forbidden" in error for error in result.errors)
|
|
|
|
|
|
def test_runtime_command_rejects_scope_substitution(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
result = command.check_command(
|
|
command.CheckCommandArgs(
|
|
command="echo local",
|
|
phase="recon",
|
|
eng_dir=str(engagement),
|
|
scope=str(tmp_path / "other-scope.yaml"),
|
|
target="10.10.10.10",
|
|
)
|
|
)
|
|
assert any("canonical scope.yaml" in error for error in result.errors)
|
|
|
|
|
|
def test_multi_task_ptt_update_validates_before_atomic_replace(tmp_path: Path) -> None:
|
|
path = tmp_path / "ptt.md"
|
|
path.write_text(
|
|
"## Phase: RECON\n\n"
|
|
"| ID | Status | Task | Notes |\n"
|
|
"|---|---|---|---|\n"
|
|
"| PT-001 | [~] | Active | original |\n"
|
|
"| PT-002 | [ ] | Next | untouched |\n",
|
|
encoding="utf-8",
|
|
)
|
|
original = path.read_text(encoding="utf-8")
|
|
with pytest.raises(ValueError, match="PT-999"):
|
|
ptt.update_tasks(path, {"PT-001": ("[x]", "done"), "PT-999": ("[~]", "bad")})
|
|
assert path.read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_concurrent_ptt_transitions_are_serialized_by_workflow_lock(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
ptt_path = engagement / "state" / "ptt.md"
|
|
|
|
def start(task_id: str) -> None:
|
|
with state.workflow_lock(engagement):
|
|
tasks = ptt.parse_ptt(ptt_path)
|
|
_start_ptt_task(
|
|
ptt_path,
|
|
tasks,
|
|
task_id,
|
|
"[~]",
|
|
f"started {task_id}",
|
|
eng_dir=engagement,
|
|
)
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
|
list(pool.map(start, ("PT-010", "PT-011")))
|
|
|
|
tasks = ptt.parse_ptt(ptt_path)
|
|
assert len([task for task in tasks if task.status == "[~]"]) == 1
|
|
assert {task.id for task in tasks} >= {"PT-010", "PT-011"}
|
|
|
|
|
|
def test_vulnerability_research_exit_blocks_unresolved_hypotheses(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md", id="001", title="Unresolved", status="Likely"
|
|
)
|
|
with pytest.raises(ValueError, match="unresolved hypotheses: H-001"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_ptt_notes_redact_credentials_before_persisting() -> None:
|
|
note = (
|
|
"JWT eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature "
|
|
"Authorization: Bearer bearer-secret "
|
|
"key sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789"
|
|
)
|
|
redacted = _redact_sensitive_note(note)
|
|
assert "eyJhbGciOiJIUzI1NiJ9" not in redacted
|
|
assert "bearer-secret" not in redacted
|
|
assert "sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789" not in redacted
|
|
assert "[REDACTED_JWT]" in redacted
|
|
assert "Bearer [REDACTED_TOKEN]" in redacted
|
|
assert "[REDACTED_API_KEY]" in redacted
|
|
|
|
|
|
def test_audit_mode_vulnerability_research_exit_requires_dispositioned_matrix(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: pending\n evidence_or_reason: ''\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError, match="undispositioned coverage: routes"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_reporting_exit_requires_canonical_finding(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
evidence = engagement / "evidence" / "exploitation" / "proof.txt"
|
|
evidence.parent.mkdir(parents=True, exist_ok=True)
|
|
evidence.write_text("decisive runtime proof\n", encoding="utf-8")
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="Validated issue",
|
|
status="Validated",
|
|
runtime_evidence="evidence/exploitation/proof.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="canonical findings: H-001"):
|
|
_validate_phase_exit(engagement, "PT-050", "[x]")
|
|
|
|
|
|
def test_reporting_exit_blocks_recon_only_run_in_audit_mode(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
history = engagement / "state" / "history.md"
|
|
history.write_text(
|
|
"# Command History\n- 2026-08-11T12:00:00Z | phase=recon | exit_code=0 | command=curl x\n"
|
|
"- 2026-08-11T12:01:00Z | phase=recon | exit_code=0 | command=curl y\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError, match="no commands were executed in EXPLOITATION"):
|
|
_validate_phase_exit(engagement, "PT-050", "[x]")
|
|
|
|
|
|
def test_reporting_exit_allows_exploitation_history_in_audit_mode(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
history = engagement / "state" / "history.md"
|
|
history.write_text(
|
|
"# Command History\n- 2026-08-11T12:00:00Z | phase=recon | exit_code=0 | command=curl x\n"
|
|
"- 2026-08-11T12:05:00Z | phase=exploitation | exit_code=0 | command=curl y\n",
|
|
encoding="utf-8",
|
|
)
|
|
evidence = engagement / "evidence" / "exploitation" / "proof.txt"
|
|
evidence.parent.mkdir(parents=True, exist_ok=True)
|
|
evidence.write_text("decisive runtime proof\n", encoding="utf-8")
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="Validated issue",
|
|
status="Validated",
|
|
runtime_evidence="evidence/exploitation/proof.txt",
|
|
)
|
|
# No exploitation-phase-history error: the run reached EXPLOITATION. It
|
|
# still blocks on the unlinked Validated hypothesis (no FIND file yet).
|
|
with pytest.raises(ValueError, match="canonical findings: H-001"):
|
|
_validate_phase_exit(engagement, "PT-050", "[x]")
|
|
|
|
|
|
def test_vuln_research_exit_requires_evidence_for_not_applicable_coverage(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n"
|
|
" rate_limits:\n"
|
|
" status: not_applicable\n"
|
|
" evidence_or_reason: 'no rate-limit behavior observed on target'\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError, match="not_applicable without evidence file"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_vuln_research_coverage_error_teaches_remediation(tmp_path: Path) -> None:
|
|
"""The undispositioned-coverage error must name a fix, not just list failures."""
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8")
|
|
+ "\nengagement:\n audit_mode: true\n coverage_obligations:\n - POST /api/route_a\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n route_a:\n status: tested\n evidence_or_reason: 'no artifact cited'\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError) as exc:
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
msg = str(exc.value)
|
|
assert "coverage-matrix cell" in msg # which obligation never got a cell
|
|
assert "how to fix" in msg # the gate teaches the remediation
|
|
assert "'not_applicable' cells" in msg
|
|
|
|
|
|
def test_vuln_research_exit_accepts_evidence_backed_not_applicable(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n"
|
|
" rate_limits:\n"
|
|
" status: not_applicable\n"
|
|
" evidence_or_reason: 'probed 10x in evidence/recon/rate_probe.txt; no 429'\n",
|
|
encoding="utf-8",
|
|
)
|
|
_validate_phase_exit(engagement, "PT-030", "[x]") # no exception
|
|
|
|
|
|
def test_vuln_research_exit_blocks_not_implemented_rejection_without_evidence(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: tested\n evidence_or_reason: 'evidence/recon/probe.txt'\n",
|
|
encoding="utf-8",
|
|
)
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="Admin login check",
|
|
status="Rejected",
|
|
verification_status="not_implemented",
|
|
test_command="N/A - placeholder hypothesis",
|
|
test_response="never executed",
|
|
rejection_reason="placeholder superseded",
|
|
cheapest_test="Login as admin (admin/admin)",
|
|
)
|
|
with pytest.raises(
|
|
ValueError, match="rejections that never ran their cheapest discriminating test"
|
|
):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_vuln_research_exit_accepts_surface_mapping_rejection_with_evidence(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Known-good pattern: a recon surface-mapping hypothesis rejected as
|
|
not_implemented is fine when it cites real bundle/probe evidence."""
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: tested\n evidence_or_reason: 'evidence/recon/probe.txt'\n",
|
|
encoding="utf-8",
|
|
)
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="API surface enumeration from JS bundle",
|
|
status="Rejected",
|
|
verification_status="not_implemented",
|
|
test_command="GET /api/v1/products/, /testimonials/",
|
|
test_response="surface mapped, see evidence",
|
|
rejection_reason="not a vulnerability claim",
|
|
evidence="evidence/recon/recon_bundle.js",
|
|
cheapest_test="Probe each derived endpoint",
|
|
)
|
|
_validate_phase_exit(engagement, "PT-030", "[x]") # no exception
|
|
|
|
|
|
def test_vuln_research_exit_blocks_validated_hypothesis_without_linked_finding(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Scorer-confirmed contract: Validated hypotheses must link FIND files."""
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: tested\n evidence_or_reason: 'evidence/recon/probe.txt'\n",
|
|
encoding="utf-8",
|
|
)
|
|
proof = engagement / "evidence" / "vuln-research" / "admin_users.txt"
|
|
proof.parent.mkdir(parents=True, exist_ok=True)
|
|
proof.write_text("200 with admin data\n", encoding="utf-8")
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="Admin ACL bypass",
|
|
status="Validated",
|
|
test_response="200 with admin data",
|
|
runtime_evidence="evidence/vuln-research/admin_users.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="every Validated hypothesis links a canonical"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_vuln_research_exit_accepts_validated_hypothesis_with_linked_finding(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: tested\n evidence_or_reason: 'evidence/recon/probe.txt'\n",
|
|
encoding="utf-8",
|
|
)
|
|
finding_dir = engagement / "evidence" / "findings"
|
|
finding_dir.mkdir(parents=True, exist_ok=True)
|
|
finding_dir.joinpath("FIND-001.md").write_text(
|
|
"# FIND-001: Admin ACL bypass\n- **Key:** value\n", encoding="utf-8"
|
|
)
|
|
proof = engagement / "evidence" / "vuln-research" / "admin_users.txt"
|
|
proof.parent.mkdir(parents=True, exist_ok=True)
|
|
proof.write_text("200 with admin data\n", encoding="utf-8")
|
|
hypotheses.update_hypothesis(
|
|
engagement / "hypotheses.md",
|
|
id="001",
|
|
title="Admin ACL bypass",
|
|
status="Validated",
|
|
test_response="200 with admin data",
|
|
runtime_evidence="evidence/vuln-research/admin_users.txt",
|
|
linked_findings="FIND-001",
|
|
)
|
|
_validate_phase_exit(engagement, "PT-030", "[x]") # no exception
|
|
|
|
|
|
def test_validated_hypothesis_rejects_escaping_or_empty_evidence(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
board = engagement / "hypotheses.md"
|
|
hypotheses.update_hypothesis(board, id="001", title="Candidate", status="Candidate")
|
|
original = board.read_text(encoding="utf-8")
|
|
outside = engagement / "outside.txt"
|
|
outside.write_text("proof\n", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="beneath evidence"):
|
|
hypotheses.update_hypothesis(
|
|
board,
|
|
id="001",
|
|
status="Validated",
|
|
runtime_evidence="evidence/../outside.txt",
|
|
)
|
|
assert board.read_text(encoding="utf-8") == original
|
|
|
|
empty = engagement / "evidence" / "exploitation" / "empty.txt"
|
|
empty.parent.mkdir(parents=True, exist_ok=True)
|
|
empty.write_text("", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
hypotheses.update_hypothesis(
|
|
board,
|
|
id="001",
|
|
status="Validated",
|
|
runtime_evidence="evidence/exploitation/empty.txt",
|
|
)
|
|
|
|
|
|
def test_vuln_research_exit_blocks_uncharted_scored_challenges(tmp_path: Path) -> None:
|
|
"""Coverage completeness: every in-scope endpoint needs a matrix cell.
|
|
|
|
Client-provided in-scope endpoints (fetch-url, login) must map to a
|
|
coverage-matrix cell — self-declared 'tested'/N/A coverage of related
|
|
categories is not enough.
|
|
"""
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8")
|
|
+ "\nengagement:\n audit_mode: true\n coverage_obligations:\n - GET /api/v1/uploads/fetch-url\n - POST /api/v1/auth/login\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n routes:\n status: tested\n evidence_or_reason: 'evidence/recon/probe.txt'\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError, match="no coverage-matrix cell"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_vuln_research_exit_blocks_aspirational_tested_narrative(tmp_path: Path) -> None:
|
|
"""'tested' without an artifact reference is aspirational, not proof."""
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8") + "\nengagement:\n audit_mode: true\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n redirects:\n status: tested\n evidence_or_reason: 'no open redirect parameter found'\n",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(ValueError, match="tested without evidence/FIND/hypothesis"):
|
|
_validate_phase_exit(engagement, "PT-030", "[x]")
|
|
|
|
|
|
def test_vuln_research_exit_accepts_challenge_cells_with_artifact(tmp_path: Path) -> None:
|
|
engagement = tmp_path / "engagement"
|
|
assert bootstrap.init_engagement(engagement, host="10.10.10.10") == 0
|
|
scope = engagement / "scope" / "scope.yaml"
|
|
scope.write_text(
|
|
scope.read_text(encoding="utf-8")
|
|
+ "\nengagement:\n audit_mode: true\n coverage_obligations:\n - POST /api/v1/auth/login\n",
|
|
encoding="utf-8",
|
|
)
|
|
matrix = engagement / "state" / "coverage-matrix.yaml"
|
|
matrix.write_text(
|
|
"coverage:\n no-rate-limiting:\n status: not_applicable\n evidence_or_reason: 'evidence/vuln-research/rate_na.txt - 429 never observed; POST /api/v1/auth/login probed 20x'\n",
|
|
encoding="utf-8",
|
|
)
|
|
_validate_phase_exit(engagement, "PT-030", "[x]") # no exception
|