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)