| 1 | from pathlib import Path |
| 2 | |
| 3 | |
| 4 | ROOT = Path(__file__).resolve().parents[1] |
| 5 | WORKFLOW = ROOT / ".github" / "workflows" / "security.yml" |
| 6 | # AGENTS.md is the canonical agent-guidance file; CLAUDE.md is a one-line |
| 7 | # pointer (`@AGENTS.md`) so anything Claude Code-shaped reads the same source. |
| 8 | AGENTS = ROOT / "AGENTS.md" |
| 9 | |
| 10 | |
| 11 | def _workflow_text() -> str: |
| 12 | return WORKFLOW.read_text(encoding="utf-8") |
| 13 | |
| 14 | |
| 15 | def test_security_workflow_exists() -> None: |
| 16 | assert WORKFLOW.is_file() |
| 17 | |
| 18 | |
| 19 | def test_security_workflow_runs_dependency_audit_as_blocking_check() -> None: |
| 20 | text = _workflow_text() |
| 21 | dependency_audit_job = text.split("dependency-audit:", 1)[1].split("secret-scan:", 1)[0] |
| 22 | |
| 23 | assert "dependency-audit:" in text |
| 24 | assert "uv audit --locked" in dependency_audit_job |
| 25 | assert "continue-on-error: true" not in dependency_audit_job |
| 26 | |
| 27 | |
| 28 | def test_security_workflow_runs_secret_scan_for_pull_requests_and_main_pushes() -> None: |
| 29 | text = _workflow_text() |
| 30 | secret_scan_job = text.split("secret-scan:", 1)[1].split("sast-scan:", 1)[0] |
| 31 | |
| 32 | assert "secret-scan:" in text |
| 33 | assert "pull_request:" in text |
| 34 | assert "push:" in text |
| 35 | assert "workflow_dispatch:" in text |
| 36 | assert "branches:\n - main" in text |
| 37 | assert "trufflesecurity/trufflehog" in secret_scan_job |
| 38 | assert "version: 3.95.5" in secret_scan_job |
| 39 | assert "extra_args: --results=verified" in secret_scan_job |
| 40 | assert "continue-on-error: true" not in secret_scan_job |
| 41 | assert "if: github.event_name" not in secret_scan_job |
| 42 | assert "path: ./" not in secret_scan_job |
| 43 | |
| 44 | |
| 45 | def test_security_workflow_runs_sast_without_unused_code_scanning_permission() -> None: |
| 46 | text = _workflow_text() |
| 47 | sast_job = text.split("sast-scan:", 1)[1] |
| 48 | |
| 49 | assert "semgrep/semgrep@sha256:" in sast_job |
| 50 | assert "semgrep scan --config=auto" in sast_job |
| 51 | assert "SEMGREP_SEND_METRICS: off" in sast_job |
| 52 | assert "continue-on-error: true" in sast_job |
| 53 | assert "contents: read" in sast_job |
| 54 | assert "security-events: write" not in sast_job |
| 55 | assert "--sarif" not in sast_job |
| 56 | assert "upload-sarif" not in sast_job |
| 57 | |
| 58 | |
| 59 | def test_agent_guidance_mentions_secret_hygiene() -> None: |
| 60 | text = AGENTS.read_text(encoding="utf-8") |
| 61 | |
| 62 | assert "Security hygiene" in text |
| 63 | assert "Never commit real API keys" in text |
| 64 | assert "skills/last30days/scripts/lib/env.py" in text |
| 65 | assert "fixtures" in text |
| 66 |