| """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)
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
| 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))
|
|
|
| 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}"
|
|
|
|
|
|
|
|
|
| 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}"
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
| 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]
|
| findings, msg = semgrep_pack(rules_path, str(d), label, category)
|
|
|
| assert isinstance(findings, list)
|
| finally:
|
| _cleanup(d)
|
|
|
|
|
|
|
|
|
| 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))
|
|
|
| assert isinstance(findings, list)
|
| finally:
|
| _cleanup(d)
|
|
|
|
|
|
|
|
|
| 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)
|
|
|