Openenv / graders /static_analysis.py
vishaldhakad's picture
intial push
eda351c
Raw
History Blame Contribute Delete
5.79 kB
"""
graders/static_analysis.py β€” Static security grader.
Weight: 15% of total reward.
Tools:
bandit: AST-based Python security scanner, zero-config, maps to CWE IDs
semgrep: Rule-based pattern matching β€” catches what bandit misses
Penalty schedule:
HIGH severity issue: -0.30
MEDIUM severity issue: -0.15
LOW severity issue: -0.05
Score = max(0.0, 1.0 - total_penalty)
No penalty stacking beyond score floor of 0.0.
"""
import subprocess
import json
import tempfile
import os
import re
from typing import Dict, Any
# ── bandit ────────────────────────────────────────────────────────────────────
def run_bandit(code: str) -> Dict[str, Any]:
"""Run bandit static analysis. Returns score + issues list."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write(code)
tmp = f.name
try:
result = subprocess.run(
["bandit", "-r", tmp, "-f", "json", "-q", "--exit-zero"],
capture_output=True, text=True, timeout=15,
)
try:
data = json.loads(result.stdout or '{"results": []}')
except json.JSONDecodeError:
data = {"results": []}
issues = data.get("results", [])
penalty = 0.0
for issue in issues:
sev = issue.get("issue_severity", "LOW")
if sev == "HIGH":
penalty += 0.30
elif sev == "MEDIUM":
penalty += 0.15
else:
penalty += 0.05
score = max(0.0, 1.0 - penalty)
return {
"score": round(score, 4),
"issues": issues[:5], # Return top 5 for feedback
"issue_count": len(issues),
}
except FileNotFoundError:
# bandit not installed β€” skip gracefully
return {"score": 0.9, "issues": [], "issue_count": 0, "note": "bandit not available"}
except subprocess.TimeoutExpired:
return {"score": 0.7, "issues": [], "issue_count": 0, "note": "bandit timeout"}
finally:
try:
os.unlink(tmp)
except OSError:
pass
# ── AST heuristics (zero-dependency fallback + extras bandit misses) ──────────
_DANGEROUS_PATTERNS = [
(r'\beval\s*\(', "HIGH", "eval() usage β€” arbitrary code execution risk"),
(r'\bexec\s*\(', "HIGH", "exec() usage β€” arbitrary code execution risk"),
(r'hashlib\.md5\b', "HIGH", "MD5 usage β€” broken cryptographic algorithm (CWE-327)"),
(r'hashlib\.sha1\b', "MEDIUM", "SHA1 usage β€” deprecated for security (CWE-327)"),
(r'random\.random\b', "MEDIUM", "random.random() β€” not cryptographically secure (use secrets)"),
(r'subprocess.*shell\s*=\s*True', "HIGH", "shell=True β€” shell injection risk (CWE-78)"),
(r'os\.system\s*\(', "HIGH", "os.system() β€” shell injection risk (CWE-78)"),
(r'pickle\.loads?\s*\(', "HIGH", "pickle β€” arbitrary code execution on untrusted data"),
(r'yaml\.load\s*\([^)]*\)', "MEDIUM", "yaml.load() without Loader β€” use yaml.safe_load()"),
(r'password\s*=\s*["\']', "MEDIUM", "Potential hardcoded password (CWE-259)"),
(r'secret\s*=\s*["\']', "MEDIUM", "Potential hardcoded secret"),
(r'f["\'].*SELECT.*\{', "HIGH", "f-string SQL construction β€” injection risk (CWE-89)"),
(r'%.*SELECT.*%', "HIGH", "%-format SQL construction β€” injection risk (CWE-89)"),
(r'\.format\(.*\).*SELECT|SELECT.*\.format', "HIGH", "str.format() SQL β€” injection risk (CWE-89)"),
]
def run_ast_heuristics(code: str) -> Dict[str, Any]:
"""Fast regex-based heuristic checks as bandit supplement."""
issues = []
for pattern, severity, message in _DANGEROUS_PATTERNS:
if re.search(pattern, code, re.IGNORECASE):
issues.append({"severity": severity, "message": message})
penalty = 0.0
for issue in issues:
if issue["severity"] == "HIGH":
penalty += 0.25
elif issue["severity"] == "MEDIUM":
penalty += 0.10
else:
penalty += 0.04
return {
"score": max(0.0, 1.0 - penalty),
"issues": issues,
}
# ── Combined grader ───────────────────────────────────────────────────────────
def grade_static(code: str) -> Dict[str, Any]:
"""
Run bandit + AST heuristics, return combined score.
Final score = min(bandit_score, heuristic_score) β€” take the more pessimistic view.
"""
bandit_result = run_bandit(code)
heuristic_result = run_ast_heuristics(code)
# Combine: worst of both tools wins
combined_score = min(bandit_result["score"], heuristic_result["score"])
all_issues = bandit_result.get("issues", []) + heuristic_result.get("issues", [])
issue_count = len(all_issues)
if combined_score >= 0.9:
feedback = "No significant static vulnerabilities detected."
elif combined_score >= 0.7:
feedback = f"{issue_count} minor issue(s) found. Review bandit output."
elif combined_score >= 0.5:
feedback = f"{issue_count} moderate issue(s). Avoid eval/exec, weak crypto, shell=True."
else:
feedback = f"{issue_count} HIGH severity issue(s). Critical: remove eval/exec, use parameterised queries, avoid MD5/SHA1."
return {
"score": round(combined_score, 4),
"feedback": feedback,
"issue_count": issue_count,
"bandit_score": bandit_result["score"],
"heuristic_score": heuristic_result["score"],
"issues": all_issues[:5],
}