File size: 7,109 Bytes
5248e3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""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)