Some checks failed
Test / pytest (pull_request) Failing after 8s
Add scripts/security_linter.py: standalone CLI that scans Python code for common security vulnerabilities with severity ratings (CRITICAL/HIGH/ MEDIUM/LOW). Outputs JSON report by default, Markdown optional. Checks include: eval/exec, subprocess shell=True, pickle, yaml.load, hardcoded secrets, weak hashes, SQL injection patterns, and dynamic imports. Add scripts/test_security_linter.py: pytest test suite validating core detection patterns and report generation. This implements the smallest concrete fix to satisfy the acceptance criteria: runs security linters, reports findings with severity, outputs security lint report. Closes #158
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for scripts/security_linter.py — Issue #158: 9.4 Security Linter."""
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
|
|
|
from security_linter import (
|
|
scan_file,
|
|
scan_directory,
|
|
generate_json_report,
|
|
generate_markdown_report,
|
|
SEVERITY_CRITICAL,
|
|
SEVERITY_HIGH,
|
|
SEVERITY_MEDIUM,
|
|
SEVERITY_LOW,
|
|
)
|
|
|
|
|
|
def test_scan_file_detects_eval():
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write("result = eval(user_input)\n")
|
|
f.flush()
|
|
findings = scan_file(Path(f.name))
|
|
assert len(findings) >= 1
|
|
assert findings[0].severity == SEVERITY_CRITICAL
|
|
assert "eval" in findings[0].issue.lower()
|
|
|
|
|
|
def test_scan_file_detects_hardcoded_password():
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write("password = 'supersecret123'\n")
|
|
f.flush()
|
|
findings = scan_file(Path(f.name))
|
|
assert any(f.severity == SEVERITY_HIGH for f in findings)
|
|
|
|
|
|
def test_scan_file_detects_subprocess_shell_true():
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write("subprocess.run(cmd, shell=True)\n")
|
|
f.flush()
|
|
findings = scan_file(Path(f.name))
|
|
assert any(f.severity == SEVERITY_HIGH and "shell" in f.issue.lower() for f in findings)
|
|
|
|
|
|
def test_scan_file_detects_pickle():
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write("data = pickle.loads(raw)\n")
|
|
f.flush()
|
|
findings = scan_file(Path(f.name))
|
|
assert any(f.severity == SEVERITY_HIGH and "pickle" in f.issue.lower() for f in findings)
|
|
|
|
|
|
def test_scan_file_detects_yaml_load():
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write("config = yaml.load(stream)\n")
|
|
f.flush()
|
|
findings = scan_file(Path(f.name))
|
|
assert any("yaml.load" in f.issue.lower() for f in findings)
|
|
|
|
|
|
def test_json_report_structure():
|
|
from security_linter import SecurityFinding
|
|
findings = [
|
|
SecurityFinding("foo.py", 1, "eval() used", SEVERITY_CRITICAL, "CWE-95", "Use ast.literal_eval"),
|
|
SecurityFinding("bar.py", 10, "hardcoded password", SEVERITY_HIGH, "CWE-259", None),
|
|
]
|
|
report = generate_json_report(findings)
|
|
assert "security_scan" in report
|
|
assert report["security_scan"]["total_findings"] == 2
|
|
assert report["security_scan"]["by_severity"][SEVERITY_CRITICAL] == 1
|
|
assert report["security_scan"]["by_severity"][SEVERITY_HIGH] == 1
|
|
|
|
|
|
def test_markdown_report_contains_severity():
|
|
from security_linter import SecurityFinding
|
|
findings = [
|
|
SecurityFinding("test.py", 1, "eval() used", SEVERITY_CRITICAL, "CWE-95", "Use ast.literal_eval"),
|
|
]
|
|
md = generate_markdown_report(findings)
|
|
assert "CRITICAL" in md or "🔴" in md
|
|
assert "eval() used" in md
|
|
assert "CWE-95" in md
|
|
|
|
|
|
def test_scan_directory_empty_dir():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
findings = scan_directory(Path(tmpdir))
|
|
assert findings == []
|
|
|
|
|
|
def test_scan_file_no_issues():
|
|
safe_code = |