File size: 4,548 Bytes
67df99f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import tempfile
import pytest

from app.tools.ast_parser import (
    detect_security_issues,
    parse_python_file,
    parse_repository,
    scan_repository_security,
)


def _write(tmpdir: str, filename: str, content: str) -> str:
    path = os.path.join(tmpdir, filename)
    with open(path, "w") as f:
        f.write(content)
    return path


# ── parse_python_file ────────────────────────────────────────────────────────


def test_parse_python_file_extracts_functions() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "f.py", "def hello(x, y):\n    pass\n")
        result = parse_python_file(p)
    assert any(fn["name"] == "hello" for fn in result["functions"])
    assert result["errors"] == []


def test_parse_python_file_extracts_classes() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "c.py", "class Foo:\n    pass\n")
        result = parse_python_file(p)
    assert any(cls["name"] == "Foo" for cls in result["classes"])


def test_parse_python_file_extracts_imports() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "i.py", "import os\nfrom pathlib import Path\n")
        result = parse_python_file(p)
    assert "os" in result["imports"]
    assert "pathlib" in result["imports"]


def test_parse_python_file_handles_syntax_error() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "bad.py", "def broken(\n")
        result = parse_python_file(p)
    assert any("SyntaxError" in e for e in result["errors"])


def test_parse_repository_skips_non_python() -> None:
    with tempfile.TemporaryDirectory() as d:
        _write(d, "readme.md", "# hi")
        _write(d, "main.py", "x = 1\n")
        results = parse_repository(d)
    assert len(results) == 1
    assert results[0]["file"].endswith("main.py")


# ── detect_security_issues ───────────────────────────────────────────────────


def test_detects_eval() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "e.py", "result = eval(user_input)\n")
        findings = detect_security_issues(p)
    rules = [f["rule"] for f in findings]
    assert "dangerous-eval" in rules


def test_detects_exec() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "e.py", "exec(user_code)\n")
        findings = detect_security_issues(p)
    rules = [f["rule"] for f in findings]
    assert "dangerous-exec" in rules


def test_detects_shell_true() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "s.py", "import subprocess\nsubprocess.run(cmd, shell=True)\n")
        findings = detect_security_issues(p)
    rules = [f["rule"] for f in findings]
    assert "shell-injection" in rules


def test_detects_hardcoded_secret() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "k.py", 'api_key = "supersecretvalue"\n')
        findings = detect_security_issues(p)
    rules = [f["rule"] for f in findings]
    assert "hardcoded-secret" in rules


def test_no_false_positive_on_clean_file() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "clean.py", "def add(a, b):\n    return a + b\n")
        findings = detect_security_issues(p)
    assert findings == []


def test_handles_syntax_error_gracefully() -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "bad.py", "def broken(\n")
        findings = detect_security_issues(p)
    assert findings == []  # returns empty, doesn't raise


def test_scan_repository_security_aggregates() -> None:
    with tempfile.TemporaryDirectory() as d:
        _write(d, "a.py", "eval(x)\n")
        _write(d, "b.py", "def safe():\n    return 1\n")
        findings = scan_repository_security(d)
    assert any(f["rule"] == "dangerous-eval" for f in findings)


@pytest.mark.parametrize("rule,code", [
    ("dangerous-eval", "eval(x)\n"),
    ("dangerous-exec", "exec(x)\n"),
    ("shell-injection", "import subprocess\nsubprocess.run(c, shell=True)\n"),
    ("hardcoded-secret", 'password = "hunter2abc"\n'),
])
def test_parametrized_detection(rule: str, code: str) -> None:
    with tempfile.TemporaryDirectory() as d:
        p = _write(d, "t.py", code)
        findings = detect_security_issues(p)
    assert any(f["rule"] == rule for f in findings)