feat(02-01): create security assessment module
Some checks failed
Discord Webhook / git (push) Has been cancelled
Some checks failed
Discord Webhook / git (push) Has been cancelled
This commit is contained in:
5
src/security/__init__.py
Normal file
5
src/security/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Security assessment and code analysis module."""
|
||||||
|
|
||||||
|
from .assessor import SecurityAssessor, SecurityLevel
|
||||||
|
|
||||||
|
__all__ = ["SecurityAssessor", "SecurityLevel"]
|
||||||
290
src/security/assessor.py
Normal file
290
src/security/assessor.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
"""Security assessment engine using Bandit and Semgrep."""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityLevel(enum.Enum):
|
||||||
|
"""Security assessment levels."""
|
||||||
|
|
||||||
|
LOW = "LOW"
|
||||||
|
MEDIUM = "MEDIUM"
|
||||||
|
HIGH = "HIGH"
|
||||||
|
BLOCKED = "BLOCKED"
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityAssessor:
|
||||||
|
"""Multi-level security assessment using Bandit and Semgrep."""
|
||||||
|
|
||||||
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
|
"""Initialize security assessor with optional configuration."""
|
||||||
|
self.config_path = config_path or "config/security.yaml"
|
||||||
|
self._load_policies()
|
||||||
|
|
||||||
|
def _load_policies(self) -> None:
|
||||||
|
"""Load security assessment policies from configuration."""
|
||||||
|
# Default policies - will be overridden by config file if exists
|
||||||
|
self.policies = {
|
||||||
|
"blocked_patterns": [
|
||||||
|
"os.system",
|
||||||
|
"subprocess.call",
|
||||||
|
"eval(",
|
||||||
|
"exec(",
|
||||||
|
"__import__",
|
||||||
|
"open(",
|
||||||
|
"file(",
|
||||||
|
"input(",
|
||||||
|
],
|
||||||
|
"high_triggers": [
|
||||||
|
"admin",
|
||||||
|
"root",
|
||||||
|
"sudo",
|
||||||
|
"passwd",
|
||||||
|
"shadow",
|
||||||
|
"system32",
|
||||||
|
"/etc/passwd",
|
||||||
|
"/etc/shadow",
|
||||||
|
],
|
||||||
|
"thresholds": {"blocked_score": 10, "high_score": 7, "medium_score": 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load config file if exists
|
||||||
|
config_file = Path(self.config_path)
|
||||||
|
if config_file.exists():
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
with open(config_file, "r") as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
self.policies.update(config.get("policies", {}))
|
||||||
|
except Exception:
|
||||||
|
# Fall back to defaults if config loading fails
|
||||||
|
pass
|
||||||
|
|
||||||
|
def assess(self, code: str) -> Tuple[SecurityLevel, Dict]:
|
||||||
|
"""Assess code security using Bandit and Semgrep.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python code to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (SecurityLevel, detailed_findings)
|
||||||
|
"""
|
||||||
|
if not code or not code.strip():
|
||||||
|
return SecurityLevel.LOW, {"message": "Empty code provided"}
|
||||||
|
|
||||||
|
findings = {
|
||||||
|
"bandit_results": [],
|
||||||
|
"semgrep_results": [],
|
||||||
|
"custom_analysis": {},
|
||||||
|
"security_score": 0,
|
||||||
|
"recommendations": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run Bandit analysis
|
||||||
|
bandit_findings = self._run_bandit(code)
|
||||||
|
findings["bandit_results"] = bandit_findings
|
||||||
|
|
||||||
|
# Run Semgrep analysis
|
||||||
|
semgrep_findings = self._run_semgrep(code)
|
||||||
|
findings["semgrep_results"] = semgrep_findings
|
||||||
|
|
||||||
|
# Custom pattern analysis
|
||||||
|
custom_findings = self._analyze_custom_patterns(code)
|
||||||
|
findings["custom_analysis"] = custom_findings
|
||||||
|
|
||||||
|
# Calculate security level
|
||||||
|
security_level, score = self._calculate_security_level(findings)
|
||||||
|
findings["security_score"] = score
|
||||||
|
|
||||||
|
# Generate recommendations
|
||||||
|
findings["recommendations"] = self._generate_recommendations(findings)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If analysis fails, be conservative
|
||||||
|
return SecurityLevel.HIGH, {
|
||||||
|
"error": f"Security analysis failed: {str(e)}",
|
||||||
|
"fallback_level": "HIGH",
|
||||||
|
}
|
||||||
|
|
||||||
|
return security_level, findings
|
||||||
|
|
||||||
|
def _run_bandit(self, code: str) -> List[Dict]:
|
||||||
|
"""Run Bandit security analysis on code."""
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write(code)
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
# Run Bandit with JSON output
|
||||||
|
result = subprocess.run(
|
||||||
|
["bandit", "-f", "json", temp_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
Path(temp_file).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
if result.returncode == 0 or result.returncode == 1: # 1 means issues found
|
||||||
|
try:
|
||||||
|
bandit_output = json.loads(result.stdout)
|
||||||
|
return bandit_output.get("results", [])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
||||||
|
# Bandit not available or failed
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _run_semgrep(self, code: str) -> List[Dict]:
|
||||||
|
"""Run Semgrep security analysis on code."""
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write(code)
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
# Run Semgrep with Python rules
|
||||||
|
result = subprocess.run(
|
||||||
|
["semgrep", "--config=p/python", "--json", temp_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
Path(temp_file).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
if result.returncode == 0 or result.returncode == 1: # 1 means findings
|
||||||
|
try:
|
||||||
|
semgrep_output = json.loads(result.stdout)
|
||||||
|
return semgrep_output.get("results", [])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
||||||
|
# Semgrep not available or failed
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _analyze_custom_patterns(self, code: str) -> Dict:
|
||||||
|
"""Analyze code for custom security patterns."""
|
||||||
|
custom_findings = {
|
||||||
|
"blocked_patterns": [],
|
||||||
|
"high_risk_patterns": [],
|
||||||
|
"suspicious_imports": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
code_lower = code.lower()
|
||||||
|
lines = code.split("\n")
|
||||||
|
|
||||||
|
# Check for blocked patterns
|
||||||
|
for i, line in enumerate(lines, 1):
|
||||||
|
for pattern in self.policies["blocked_patterns"]:
|
||||||
|
if pattern in line:
|
||||||
|
custom_findings["blocked_patterns"].append(
|
||||||
|
{"line": i, "pattern": pattern, "content": line.strip()}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for high-risk patterns
|
||||||
|
for i, line in enumerate(lines, 1):
|
||||||
|
for trigger in self.policies["high_triggers"]:
|
||||||
|
if trigger in code_lower and trigger in line.lower():
|
||||||
|
custom_findings["high_risk_patterns"].append(
|
||||||
|
{"line": i, "trigger": trigger, "content": line.strip()}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for suspicious imports
|
||||||
|
import_keywords = [
|
||||||
|
"import os",
|
||||||
|
"import sys",
|
||||||
|
"import subprocess",
|
||||||
|
"import socket",
|
||||||
|
]
|
||||||
|
for keyword in import_keywords:
|
||||||
|
if keyword in code_lower:
|
||||||
|
custom_findings["suspicious_imports"].append(keyword)
|
||||||
|
|
||||||
|
return custom_findings
|
||||||
|
|
||||||
|
def _calculate_security_level(self, findings: Dict) -> Tuple[SecurityLevel, int]:
|
||||||
|
"""Calculate security level based on all findings."""
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# Score Bandit findings
|
||||||
|
for result in findings.get("bandit_results", []):
|
||||||
|
severity = result.get("issue_severity", "LOW").upper()
|
||||||
|
if severity == "HIGH":
|
||||||
|
score += 3
|
||||||
|
elif severity == "MEDIUM":
|
||||||
|
score += 2
|
||||||
|
else:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# Score Semgrep findings
|
||||||
|
for result in findings.get("semgrep_results", []):
|
||||||
|
# Semgrep uses different severity levels
|
||||||
|
metadata = result.get("metadata", {})
|
||||||
|
severity = metadata.get("severity", "INFO").upper()
|
||||||
|
if severity == "ERROR":
|
||||||
|
score += 3
|
||||||
|
elif severity == "WARNING":
|
||||||
|
score += 2
|
||||||
|
else:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# Score custom findings
|
||||||
|
custom = findings.get("custom_analysis", {})
|
||||||
|
score += len(custom.get("blocked_patterns", [])) * 5
|
||||||
|
score += len(custom.get("high_risk_patterns", [])) * 3
|
||||||
|
score += len(custom.get("suspicious_imports", [])) * 1
|
||||||
|
|
||||||
|
# Determine security level
|
||||||
|
thresholds = self.policies["thresholds"]
|
||||||
|
if score >= thresholds["blocked_score"]:
|
||||||
|
return SecurityLevel.BLOCKED, score
|
||||||
|
elif score >= thresholds["high_score"]:
|
||||||
|
return SecurityLevel.HIGH, score
|
||||||
|
elif score >= thresholds["medium_score"]:
|
||||||
|
return SecurityLevel.MEDIUM, score
|
||||||
|
else:
|
||||||
|
return SecurityLevel.LOW, score
|
||||||
|
|
||||||
|
def _generate_recommendations(self, findings: Dict) -> List[str]:
|
||||||
|
"""Generate security recommendations based on findings."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# Analyze Bandit findings
|
||||||
|
for result in findings.get("bandit_results", []):
|
||||||
|
test_name = result.get("test_name", "")
|
||||||
|
if "hardcoded" in test_name.lower():
|
||||||
|
recommendations.append("Remove hardcoded credentials or secrets")
|
||||||
|
elif "shell" in test_name.lower():
|
||||||
|
recommendations.append(
|
||||||
|
"Avoid shell command execution, use safer alternatives"
|
||||||
|
)
|
||||||
|
elif "pickle" in test_name.lower():
|
||||||
|
recommendations.append("Avoid using pickle for untrusted data")
|
||||||
|
|
||||||
|
# Analyze custom findings
|
||||||
|
custom = findings.get("custom_analysis", {})
|
||||||
|
if custom.get("blocked_patterns"):
|
||||||
|
recommendations.append("Remove or sanitize dangerous function calls")
|
||||||
|
if custom.get("high_risk_patterns"):
|
||||||
|
recommendations.append("Review and justify high-risk system operations")
|
||||||
|
if custom.get("suspicious_imports"):
|
||||||
|
recommendations.append("Validate necessity of system-level imports")
|
||||||
|
|
||||||
|
if not recommendations:
|
||||||
|
recommendations.append("Code appears safe from common security issues")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
Reference in New Issue
Block a user