Files

150 lines
5.1 KiB
Python
Raw Permalink Normal View History

"""Tests for the closeout artifact generator (FIND parsing + findings.yaml/report.md derivation)."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
import yaml
from plugins.violin_guard.findings import (
generate_findings_yaml,
generate_report_md,
parse_finding_file,
)
FIND_MD = """# FIND-002: IDOR - arbitrary order read
- **Severity:** High
- **Batch:** 356aff5b-1674-4693-8417-96d2e9b55248
- **PTT task:** PT-102
- **Phase:** VULN_RESEARCH
- **Hypothesis:** H-003
## Description
GET /api/v1/orders/{id} performs no ownership check.
## Impact
Horizontal privilege escalation.
## Evidence
- `evidence/executions/2026-08-10T094638-ee4dead5-command.json`
- `evidence/executions/2026-08-10T094638-ee4dead5-command.stdout.txt`
## Remediation
Enforce object-ownership authorization.
"""
# ---------------------------------------------------------------------------
# Task 1: FIND file parser
# ---------------------------------------------------------------------------
def test_parse_finding_file_extracts_fields(tmp_path: Path) -> None:
f = tmp_path / "FIND-002.md"
f.write_text(FIND_MD, encoding="utf-8")
rec = parse_finding_file(f)
assert rec["id"] == "FIND-002"
assert rec["title"] == "IDOR - arbitrary order read"
assert rec["severity"] == "High"
assert rec["hypothesis"] == "H-003"
assert rec["phase"] == "VULN_RESEARCH"
assert rec["ptt_task"] == "PT-102"
assert rec["batch"] == "356aff5b-1674-4693-8417-96d2e9b55248"
assert rec["evidence"] == [
"evidence/executions/2026-08-10T094638-ee4dead5-command.json",
"evidence/executions/2026-08-10T094638-ee4dead5-command.stdout.txt",
]
assert "no ownership check" in rec["description"]
assert "object-ownership" in rec["remediation"]
def test_parse_finding_file_rejects_non_finding(tmp_path: Path) -> None:
f = tmp_path / "notes.md"
f.write_text("# Not a finding\n", encoding="utf-8")
with pytest.raises(ValueError):
parse_finding_file(f)
# ---------------------------------------------------------------------------
# Task 2: findings.yaml generator
# ---------------------------------------------------------------------------
def test_generate_findings_yaml_roundtrip(tmp_path: Path) -> None:
findings_dir = tmp_path / "evidence" / "findings"
findings_dir.mkdir(parents=True)
(findings_dir / "FIND-001.md").write_text(
FIND_MD.replace("FIND-002", "FIND-001"), encoding="utf-8"
)
out = generate_findings_yaml(tmp_path)
assert out == tmp_path / "evidence" / "reporting" / "findings.yaml"
data = yaml.safe_load(out.read_text(encoding="utf-8"))
assert data["findings"][0]["id"] == "FIND-001"
assert data["findings"][0]["severity"] == "High"
assert data["findings"][0]["hypothesis"] == "H-003"
assert len(data["findings"][0]["evidence"]) == 2
assert "generated_from" in data
def test_generate_findings_yaml_no_findings(tmp_path: Path) -> None:
with pytest.raises(ValueError):
generate_findings_yaml(tmp_path)
# ---------------------------------------------------------------------------
# Task 3: report.md generator
# ---------------------------------------------------------------------------
def test_generate_report_md_contents(tmp_path: Path) -> None:
findings_dir = tmp_path / "evidence" / "findings"
findings_dir.mkdir(parents=True)
(findings_dir / "FIND-002.md").write_text(FIND_MD, encoding="utf-8")
out = generate_report_md(tmp_path, target="https://example.test")
text = out.read_text(encoding="utf-8")
assert out == tmp_path / "reporting" / "report.md"
assert "https://example.test" in text
assert "FIND-002" in text
assert "| High | 1 |" in text
assert "no ownership check" in text
assert "Executive Summary" in text
def test_generate_report_md_no_overwrite(tmp_path: Path) -> None:
rep = tmp_path / "reporting" / "report.md"
rep.parent.mkdir(parents=True)
rep.write_text("existing", encoding="utf-8")
with pytest.raises(ValueError):
generate_report_md(tmp_path, target="https://example.test")
# ---------------------------------------------------------------------------
# Task 4: CLI subcommand
# ---------------------------------------------------------------------------
def test_cli_generate_closeout(tmp_path: Path) -> None:
findings_dir = tmp_path / "evidence" / "findings"
findings_dir.mkdir(parents=True)
(findings_dir / "FIND-001.md").write_text(
FIND_MD.replace("FIND-002", "FIND-001"), encoding="utf-8"
)
repo_root = Path(__file__).resolve().parents[2]
r = subprocess.run(
[
sys.executable,
"scripts/violin_guard.py",
"generate-closeout",
"--eng-dir",
str(tmp_path),
"--target",
"https://example.test",
],
capture_output=True,
text=True,
cwd=str(repo_root),
)
assert r.returncode == 0, r.stderr
assert (tmp_path / "evidence" / "reporting" / "findings.yaml").exists()
assert (tmp_path / "reporting" / "report.md").exists()