Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
"""SecurityAgent - Security monitoring for VoIdea.
|
||||
|
||||
Scans code for:
|
||||
- XSS, SQLi, CSRF, SSTI, path traversal, command injection
|
||||
- Hardcoded secrets, CSP issues, .env exposure, rate limit bypass
|
||||
- Dependency vulnerabilities (pip-audit)
|
||||
- 152-FZ compliance
|
||||
- LogEntry analysis (last 24h)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.base import AgentResult, AgentStatus, AgentTrigger, BaseAgent
|
||||
from app.core.config import get_settings
|
||||
from app.models.log import LogEntry
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class SecurityAgent(BaseAgent):
|
||||
"""Security monitoring and vulnerability scanning agent."""
|
||||
|
||||
name = "security_agent"
|
||||
version = "1.0.0"
|
||||
description = "Monitors security, scans vulnerabilities, enforces 152-FZ compliance"
|
||||
triggers = [
|
||||
AgentTrigger.MANUAL,
|
||||
AgentTrigger.PRE_COMMIT,
|
||||
AgentTrigger.CRON,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.project_root = Path(__file__).parent.parent.parent
|
||||
|
||||
def _get_db(self, context: dict[str, Any] | None = None) -> AsyncSession | None:
|
||||
return (context or {}).get("db", None)
|
||||
|
||||
async def run(self, context: dict[str, Any] | None = None) -> AgentResult:
|
||||
"""Execute security task.
|
||||
|
||||
Context can contain:
|
||||
- action: str (full, scan, dependencies, compliance, logs)
|
||||
- paths: list[str] (paths to scan)
|
||||
- db: AsyncSession (for log analysis)
|
||||
"""
|
||||
await self.set_running("security_check")
|
||||
|
||||
try:
|
||||
action = context.get("action", "full") if context else "full"
|
||||
paths = context.get("paths", ["app"])
|
||||
db = context.get("db", None)
|
||||
|
||||
if action == "full":
|
||||
result = await self._run_full_security_check(paths, db)
|
||||
elif action == "scan":
|
||||
result = await self._scan_vulnerabilities(paths)
|
||||
elif action == "dependencies":
|
||||
result = await self._check_dependencies()
|
||||
elif action == "compliance":
|
||||
result = await self._check_152_fz_compliance()
|
||||
elif action == "logs":
|
||||
result = await self._analyze_security_logs(db)
|
||||
else:
|
||||
result = await self._run_full_security_check(paths, db)
|
||||
|
||||
await self.set_idle()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
await self.set_error(str(e))
|
||||
return AgentResult(
|
||||
success=False,
|
||||
message=f"SecurityAgent failed: {str(e)}",
|
||||
errors=[str(e)],
|
||||
)
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if SecurityAgent is operational."""
|
||||
return self.project_root.exists()
|
||||
|
||||
async def _run_full_security_check(self, paths: list[str], db: AsyncSession | None = None) -> AgentResult:
|
||||
"""Run complete security audit."""
|
||||
vuln_result = await self._scan_vulnerabilities(paths)
|
||||
dep_result = await self._check_dependencies()
|
||||
compliance_result = await self._check_152_fz_compliance()
|
||||
log_result = await self._analyze_security_logs(db)
|
||||
|
||||
critical_issues = []
|
||||
critical_issues.extend(vuln_result.data.get("critical", []))
|
||||
critical_issues.extend(dep_result.data.get("critical", []))
|
||||
log_alerts = log_result.data.get("alerts", [])
|
||||
|
||||
passed = len(critical_issues) == 0 and compliance_result.success and len(log_alerts) == 0
|
||||
|
||||
return AgentResult(
|
||||
success=passed,
|
||||
message=f"Security check {'passed' if passed else 'failed'}: {len(critical_issues)} critical issues, {len(log_alerts)} log alerts",
|
||||
data={
|
||||
"vulnerabilities": vuln_result.data,
|
||||
"dependencies": dep_result.data,
|
||||
"compliance": compliance_result.data,
|
||||
"log_analysis": log_result.data,
|
||||
"total_critical": len(critical_issues),
|
||||
},
|
||||
)
|
||||
|
||||
async def _scan_vulnerabilities(self, paths: list[str]) -> AgentResult:
|
||||
"""Scan code for common vulnerabilities."""
|
||||
issues = {"critical": [], "high": [], "medium": [], "low": []}
|
||||
|
||||
patterns = {
|
||||
"critical": [
|
||||
(r"eval\s*\(", "Dangerous use of eval()"),
|
||||
(r"os\.system\s*\(", "Dangerous use of os.system()"),
|
||||
(r"subprocess\.call\s*.*shell\s*=\s*True", "Shell injection vulnerability"),
|
||||
(r"exec\s*\(", "Dangerous use of exec()"),
|
||||
(r"__import__\s*\(", "Dynamic import detected"),
|
||||
(r"pickle\.loads\s*\(", "Insecure deserialization (pickle)"),
|
||||
(r"sqlalchemy\.text\s*\([^)]*\+", "Raw SQL concatenation, possible SQLi"),
|
||||
(r"\.execute\s*\([^)]*f\s*['\"]", "SQL injection risk in execute()"),
|
||||
],
|
||||
"high": [
|
||||
(r"password\s*=\s*['\"][^'\"]{1,8}['\"]", "Hardcoded password detected"),
|
||||
(r"api[_-]?key\s*=\s*['\"][A-Za-z0-9]{20,}['\"]", "Potential API key in code"),
|
||||
(r"secret[_-]?key\s*=\s*['\"][^'\"]{8,}['\"]", "Possible secret key in code"),
|
||||
(r"token\s*=\s*['\"][A-Za-z0-9_-]{20,}['\"]", "Hardcoded token detected"),
|
||||
(r"exec_command|exec_cmd", "Arbitrary command execution pattern"),
|
||||
(r"request\.remote_addr|request\.environ", "IP address exposure"),
|
||||
(r"render_template_string\s*\(", "SSTI (Server-Side Template Injection) risk"),
|
||||
(r"csrf_exempt|@csrf\.exempt", "CSRF protection disabled"),
|
||||
],
|
||||
"medium": [
|
||||
(r"\.format\s*\([^)]*\.\s*(password|token|secret)", "String formatting with secrets"),
|
||||
(r"print\s*\([^)]*password", "Password being printed"),
|
||||
(r"\.env\s*released|\.env\s*exposed", "Environment file exposure"),
|
||||
(r"open\s*\([^)]*\.\./", "Path traversal risk"),
|
||||
(r"mark_safe|autoescape\s*False|autoescape\s*off", "CSP/XSS risk"),
|
||||
(r"RateLimiter|rate_limit", "Rate limit bypass pattern"),
|
||||
(r"secure=False|ssl_require=False", "SSL/TLS disabled"),
|
||||
],
|
||||
"low": [
|
||||
(r"passlib", "Consider using more secure hashing"),
|
||||
(r"debug\s*=\s*True", "Debug mode enabled"),
|
||||
(r"ALLOWED_HOSTS\s*=\s*\[.+\]", "Overly permissive ALLOWED_HOSTS"),
|
||||
(r"CORS_ORIGIN_ALLOW_ALL\s*=\s*True", "CORS allows all origins"),
|
||||
],
|
||||
}
|
||||
|
||||
for path in paths:
|
||||
path_obj = self.project_root / path
|
||||
if not path_obj.exists():
|
||||
continue
|
||||
|
||||
for file in path_obj.rglob("*.py"):
|
||||
if file.name.startswith("test_"):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = file.read_text(encoding="utf-8", errors="ignore")
|
||||
for level, pattern_list in patterns.items():
|
||||
for pattern, description in pattern_list:
|
||||
if re.search(pattern, content, re.IGNORECASE):
|
||||
issues[level].append({
|
||||
"file": str(file.relative_to(self.project_root)),
|
||||
"description": description,
|
||||
"pattern": pattern,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
has_critical = len(issues["critical"]) > 0
|
||||
|
||||
return AgentResult(
|
||||
success=not has_critical,
|
||||
message=f"Vulnerability scan: {sum(len(v) for v in issues.values())} issues",
|
||||
data=issues,
|
||||
)
|
||||
|
||||
async def _check_dependencies(self) -> AgentResult:
|
||||
"""Check dependencies for known vulnerabilities."""
|
||||
issues = {"critical": [], "high": [], "medium": [], "low": []}
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pip", "audit", "--format=json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(self.project_root),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return AgentResult(
|
||||
success=True,
|
||||
message="No vulnerable dependencies",
|
||||
data=issues,
|
||||
)
|
||||
|
||||
try:
|
||||
import json
|
||||
audit_data = json.loads(result.stdout)
|
||||
for vuln in audit_data.get("vulnerabilities", []):
|
||||
severity = vuln.get("vulns", [{}])[0].get("advisory_severity", "medium")
|
||||
if severity not in issues:
|
||||
severity = "medium"
|
||||
issues[severity].append({
|
||||
"package": vuln.get("name"),
|
||||
"version": vuln.get("version"),
|
||||
"advisory": vuln.get("advisory_id"),
|
||||
})
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass
|
||||
|
||||
except FileNotFoundError:
|
||||
return AgentResult(
|
||||
success=True,
|
||||
message="pip-audit not installed, skipping dependency check",
|
||||
data={"skipped": True},
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return AgentResult(
|
||||
success=False,
|
||||
message="Dependency check timed out",
|
||||
data={"error": "timeout"},
|
||||
)
|
||||
except Exception:
|
||||
return AgentResult(
|
||||
success=True,
|
||||
message="Could not run dependency check",
|
||||
data={"skipped": True},
|
||||
)
|
||||
|
||||
has_critical = len(issues["critical"]) > 0 or len(issues["high"]) > 0
|
||||
|
||||
return AgentResult(
|
||||
success=not has_critical,
|
||||
message=f"Dependency check: {sum(len(v) for v in issues.values())} issues",
|
||||
data=issues,
|
||||
)
|
||||
|
||||
async def _check_152_fz_compliance(self) -> AgentResult:
|
||||
"""Check compliance with 152-FZ (personal data protection)."""
|
||||
issues = []
|
||||
warnings = []
|
||||
|
||||
check_items = [
|
||||
{
|
||||
"pattern": r"email.*varchar\(255\)",
|
||||
"check": "Email field length adequate",
|
||||
"severity": "info",
|
||||
},
|
||||
{
|
||||
"pattern": r"password.*varchar",
|
||||
"check": "Password field exists",
|
||||
"severity": "info",
|
||||
},
|
||||
{
|
||||
"pattern": r"bcrypt|passlib",
|
||||
"check": "Password hashing implemented",
|
||||
"severity": "info",
|
||||
},
|
||||
{
|
||||
"pattern": r"jwt|JWT",
|
||||
"check": "JWT authentication present",
|
||||
"severity": "info",
|
||||
},
|
||||
]
|
||||
|
||||
models_dir = self.project_root / "app" / "models"
|
||||
if models_dir.exists():
|
||||
for file in models_dir.rglob("*.py"):
|
||||
if file.name.startswith("test_"):
|
||||
continue
|
||||
try:
|
||||
content = file.read_text(encoding="utf-8", errors="ignore")
|
||||
for item in check_items:
|
||||
if re.search(item["pattern"], content, re.IGNORECASE):
|
||||
warnings.append({
|
||||
"file": str(file.relative_to(self.project_root)),
|
||||
"check": item["check"],
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return AgentResult(
|
||||
success=True,
|
||||
message=f"152-FZ compliance: {len(warnings)} checks passed",
|
||||
data={
|
||||
"checks_passed": len(warnings),
|
||||
"issues": issues,
|
||||
"warnings": warnings,
|
||||
},
|
||||
)
|
||||
|
||||
async def _analyze_security_logs(self, db: AsyncSession | None = None) -> AgentResult:
|
||||
"""Analyze security logs from LogEntry for the last 24 hours."""
|
||||
alerts = []
|
||||
if not db:
|
||||
return AgentResult(
|
||||
success=True,
|
||||
message="No DB session, log analysis skipped",
|
||||
data={"alerts": [], "total_logs": 0},
|
||||
)
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
result = await db.execute(
|
||||
select(LogEntry)
|
||||
.where(LogEntry.created_at >= cutoff)
|
||||
.order_by(LogEntry.created_at.desc())
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
for log in logs:
|
||||
msg = log.message.lower()
|
||||
if any(kw in msg for kw in ["failed login", "invalid token", "unauthorized", "brute", "rate limit"]):
|
||||
alerts.append({
|
||||
"message": log.message,
|
||||
"level": log.level,
|
||||
"source": log.source,
|
||||
"timestamp": log.created_at.isoformat(),
|
||||
})
|
||||
elif log.level == "ERROR" and "security" in (log.source or "").lower():
|
||||
alerts.append({
|
||||
"message": log.message,
|
||||
"level": log.level,
|
||||
"source": log.source,
|
||||
"timestamp": log.created_at.isoformat(),
|
||||
})
|
||||
|
||||
return AgentResult(
|
||||
success=len(alerts) == 0,
|
||||
message=f"Анализ логов за 24ч: {len(logs)} записей, {len(alerts)} предупреждений",
|
||||
data={
|
||||
"total_logs": len(logs),
|
||||
"alerts": alerts,
|
||||
},
|
||||
)
|
||||
|
||||
async def get_metrics(self) -> dict[str, Any]:
|
||||
"""Get SecurityAgent metrics."""
|
||||
return {
|
||||
"agent_id": self.name,
|
||||
"version": self.version,
|
||||
"status": self.status.value,
|
||||
"last_run": self.last_run.isoformat() if self.last_run else None,
|
||||
}
|
||||
Reference in New Issue
Block a user