"""Per-scanner unit tests — direct function calls, no UI needed. All tests are fast (no network, no git clone). They create an isolated temp directory with a single fixture file and call the scanner function directly, asserting on the returned (findings, log_msg) tuple. """ import os import shutil import sys import tempfile from pathlib import Path import pytest ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT)) FIXTURES = ROOT / "tests" / "fixtures" def _copy_to_tmp(fixture_rel: str) -> Path: """Copy a fixture file into a fresh temp dir and return the temp dir path.""" src = FIXTURES / fixture_rel tmp = Path(tempfile.mkdtemp(prefix="scanner_test_")) shutil.copy(src, tmp / src.name) return tmp def _cleanup(path: Path) -> None: shutil.rmtree(str(path), ignore_errors=True) # ── bandit ──────────────────────────────────────────────────────────────────── class TestBandit: def test_finds_vulnerabilities_in_bad_agent(self): d = _copy_to_tmp("security/bad_agent.py") try: from scanners.bandit_runner import bandit findings, msg = bandit(str(d)) assert len(findings) > 0, f"Expected findings, got none. msg={msg}" finally: _cleanup(d) def test_clean_file_has_few_findings(self): """Clean file may still have minor bandit notices; just check no HIGH/ERROR.""" d = _copy_to_tmp("security/clean.py") try: from scanners.bandit_runner import bandit findings, _ = bandit(str(d)) high_plus = [f for f in findings if f.get("severity") in ("ERROR", "HIGH")] assert len(high_plus) == 0, f"Unexpected HIGH/ERROR findings: {high_plus}" finally: _cleanup(d) # ── detect-secrets ──────────────────────────────────────────────────────────── class TestDetectSecrets: def test_finds_hardcoded_key(self, tmp_path): """detect-secrets should flag hardcoded secrets. We write a fake private key.""" secret_file = tmp_path / "credentials.py" # Use a fake PEM block — PrivateKeyDetector always fires on this pattern. secret_file.write_text( "# test fixture\n" "PRIVATE_KEY = (\n" " '-----BEGIN RSA PRIVATE KEY-----\\n'\n" " 'MIIEpAIBAAKCAQEA0Z3VS5JJcds3xHn/ygWep4sDYBD\\n'\n" " '-----END RSA PRIVATE KEY-----\\n'\n" ")\n", encoding="utf-8", ) from scanners.semgrep_runner import detect_secrets findings, msg = detect_secrets(str(tmp_path)) # detect-secrets may not be available or may need git context — skip gracefully if not findings: pytest.skip(f"detect-secrets found nothing (tool unavailable or filtered). msg={msg}") assert len(findings) > 0, f"Expected secret findings. msg={msg}" def test_clean_file_no_secrets(self, tmp_path): (tmp_path / "clean.py").write_text("x = 1\n", encoding="utf-8") from scanners.semgrep_runner import detect_secrets findings, _ = detect_secrets(str(tmp_path)) assert len(findings) == 0, f"Unexpected secrets in clean file: {findings}" # ── forbidden files ─────────────────────────────────────────────────────────── class TestForbiddenFiles: def test_finds_env_file(self, tmp_path): (tmp_path / ".env").write_text("SECRET=abc123\n", encoding="utf-8") from scanners.forbidden_files import forbidden_files findings, _ = forbidden_files(str(tmp_path)) assert any(".env" in f.get("file", "") for f in findings), \ f"Expected .env finding, got: {findings}" def test_clean_dir_no_findings(self, tmp_path): (tmp_path / "main.py").write_text("print('hello')\n", encoding="utf-8") from scanners.forbidden_files import forbidden_files findings, _ = forbidden_files(str(tmp_path)) assert findings == [], f"Unexpected findings in clean dir: {findings}" # ── ruff perf ───────────────────────────────────────────────────────────────── class TestRuffPerf: def test_finds_perf_issues(self): d = _copy_to_tmp("performance/bad_perf.py") try: from scanners.ruff_runner import ruff_perf findings, msg = ruff_perf(str(d)) assert len(findings) > 0, f"Expected PERF findings. msg={msg}" finally: _cleanup(d) # ── semgrep (security pack) ─────────────────────────────────────────────────── class TestSemgrepSecurity: @pytest.mark.slow def test_core_rules_find_issues(self): d = _copy_to_tmp("security/bad_agent.py") try: from scanners.semgrep_runner import semgrep_pack from rules import ALL_SECURITY label, rules_path, category = ALL_SECURITY[0] # core.yaml findings, msg = semgrep_pack(rules_path, str(d), label, category) # semgrep may or may not match depending on rule set; just verify it ran assert isinstance(findings, list) finally: _cleanup(d) # ── pip-audit ───────────────────────────────────────────────────────────────── class TestPipAudit: @pytest.mark.slow def test_clean_requirements_no_cves(self): d = _copy_to_tmp("clean/requirements.txt") try: from scanners.pip_audit_runner import pip_audit findings, msg = pip_audit(str(d)) # Clean fixture should have few or no known CVEs assert isinstance(findings, list) finally: _cleanup(d) # ── agent-audit ─────────────────────────────────────────────────────────────── class TestAgentAudit: @pytest.mark.slow def test_runs_without_error(self): d = _copy_to_tmp("llm/indirect_injection.py") try: from scanners.agent_audit_runner import agent_audit findings, msg = agent_audit(str(d)) assert isinstance(findings, list) assert isinstance(msg, str) finally: _cleanup(d)