Initial commit: VoIdeaAI - voice-first AI idea assistant

This commit is contained in:
2026-05-13 12:51:42 +03:00
commit 688d043dad
421 changed files with 47915 additions and 0 deletions
+355
View File
@@ -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,
}