Files
calvana/ayn-antivirus/ayn_antivirus/dashboard/api.py

1160 lines
41 KiB
Python

"""AYN Antivirus Dashboard — REST API Handlers."""
from __future__ import annotations
import asyncio
import json
import logging
import platform
import time
from datetime import datetime
from aiohttp import web
logger = logging.getLogger("ayn_antivirus.dashboard.api")
def setup_routes(app: web.Application) -> None:
"""Register all API routes on the aiohttp app."""
app.router.add_get("/api/health", handle_health)
app.router.add_get("/api/status", handle_status)
app.router.add_get("/api/threats", handle_threats)
app.router.add_get("/api/threat-stats", handle_threat_stats)
app.router.add_get("/api/scans", handle_scans)
app.router.add_get("/api/scan-chart", handle_scan_chart)
app.router.add_get("/api/quarantine", handle_quarantine)
app.router.add_get("/api/signatures", handle_signatures)
app.router.add_get("/api/sig-updates", handle_sig_updates)
app.router.add_get("/api/definitions", handle_definitions)
app.router.add_get("/api/logs", handle_logs)
app.router.add_get("/api/metrics-history", handle_metrics_history)
# Action endpoints
app.router.add_post("/api/actions/quick-scan", handle_action_quick_scan)
app.router.add_post("/api/actions/full-scan", handle_action_full_scan)
app.router.add_post("/api/actions/update-sigs", handle_action_update_sigs)
app.router.add_post("/api/actions/update-feed", handle_action_update_feed)
# Threat action endpoints
app.router.add_post("/api/actions/quarantine", handle_action_quarantine)
app.router.add_post("/api/actions/delete-threat", handle_action_delete_threat)
app.router.add_post("/api/actions/whitelist", handle_action_whitelist)
app.router.add_post("/api/actions/restore", handle_action_restore)
app.router.add_post("/api/actions/ai-analyze", handle_action_ai_analyze)
# Container endpoints
app.router.add_get("/api/containers", handle_containers)
app.router.add_get("/api/container-scan", handle_container_scan_results)
app.router.add_post("/api/actions/scan-containers", handle_action_scan_containers)
app.router.add_post("/api/actions/scan-container", handle_action_scan_single_container)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _json(data: object, status: int = 200) -> web.Response:
return web.json_response(data, status=status)
def _get_ai_analyzer(app):
"""Lazy-init the AI analyzer singleton on the app."""
if "_ai_analyzer" not in app:
from ayn_antivirus.detectors.ai_analyzer import AIAnalyzer
import os
key = os.environ.get("ANTHROPIC_API_KEY", "")
app["_ai_analyzer"] = AIAnalyzer(api_key=key) if key else None
return app.get("_ai_analyzer")
def _ai_filter_threats(app, store, threats_data: list) -> list:
"""Run AI analysis on detections. Returns only real threats."""
ai = _get_ai_analyzer(app)
if not ai or not ai.available:
return threats_data # No AI — pass all through
filtered = []
for t in threats_data:
verdict = ai.analyze(
file_path=t["file_path"],
threat_name=t["threat_name"],
threat_type=t["threat_type"],
severity=t["severity"],
detector=t["detector"],
confidence=t.get("confidence", 50),
)
t["ai_verdict"] = verdict.verdict
t["ai_confidence"] = verdict.confidence
t["ai_reason"] = verdict.reason
t["ai_action"] = verdict.recommended_action
if verdict.is_safe:
store.log_activity(
f"AI dismissed: {t['file_path']} ({t['threat_name']}) — {verdict.reason}",
"INFO", "ai_analyzer",
)
continue # Skip false positive
filtered.append(t)
dismissed = len(threats_data) - len(filtered)
if dismissed:
store.log_activity(
f"AI filtered {dismissed}/{len(threats_data)} false positives",
"INFO", "ai_analyzer",
)
return filtered
def _auto_quarantine(store, vault, file_path: str, threat_name: str, severity: str) -> str:
"""Quarantine a file automatically. Returns quarantine ID or empty string."""
if not vault:
return ""
import os
if not os.path.isfile(file_path):
return ""
try:
qid = vault.quarantine_file(
file_path=file_path,
threat_name=threat_name,
threat_type="auto",
severity=severity,
)
store.log_activity(
f"Auto-quarantined: {file_path} ({threat_name})",
"WARNING", "quarantine",
)
return qid
except Exception as exc:
logger.warning("Auto-quarantine failed for %s: %s", file_path, exc)
return ""
def _safe_int(
val: str, default: int, min_val: int = 1, max_val: int = 1000,
) -> int:
"""Parse an integer query param with clamping and fallback."""
try:
n = int(val)
return max(min_val, min(n, max_val))
except (ValueError, TypeError):
return default
def _threat_type_str(tt: object) -> str:
"""Convert a ThreatType enum (or anything) to a string."""
return tt.name if hasattr(tt, "name") else str(tt)
def _severity_str(sev: object) -> str:
"""Convert a Severity enum (or anything) to a string."""
return sev.name if hasattr(sev, "name") else str(sev)
# ------------------------------------------------------------------
# Read-only endpoints
# ------------------------------------------------------------------
async def handle_health(request: web.Request) -> web.Response:
"""GET /api/health - System health metrics (live snapshot)."""
collector = request.app["collector"]
snapshot = await asyncio.to_thread(collector.get_snapshot)
return _json(snapshot)
async def handle_status(request: web.Request) -> web.Response:
"""GET /api/status - Protection status overview."""
store = request.app["store"]
def _get() -> dict:
threat_stats = store.get_threat_stats()
scans = store.get_recent_scans(1)
sig_stats = store.get_sig_stats()
latest_metrics = store.get_latest_metrics()
quarantine_count = 0
try:
vault = request.app.get("vault")
if vault:
quarantine_count = vault.count()
except Exception:
pass
try:
import psutil
uptime_secs = int(time.time() - psutil.boot_time())
except Exception:
uptime_secs = 0
last_scan = scans[0] if scans else None
return {
"hostname": platform.node(),
"os": f"{platform.system()} {platform.release()}",
"arch": platform.machine(),
"uptime_seconds": uptime_secs,
"protection_active": True,
"last_scan": last_scan,
"threats": threat_stats,
"signatures": sig_stats,
"quarantine_count": quarantine_count,
"metrics": latest_metrics,
"server_time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
}
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_threats(request: web.Request) -> web.Response:
"""GET /api/threats?limit=50 - Recent threats list."""
store = request.app["store"]
limit = _safe_int(request.query.get("limit", "50"), 50, max_val=500)
threats = await asyncio.to_thread(store.get_recent_threats, limit)
return _json({"threats": threats, "count": len(threats)})
async def handle_threat_stats(request: web.Request) -> web.Response:
"""GET /api/threat-stats - Threat statistics."""
store = request.app["store"]
stats = await asyncio.to_thread(store.get_threat_stats)
return _json(stats)
async def handle_scans(request: web.Request) -> web.Response:
"""GET /api/scans?limit=30 - Recent scan history."""
store = request.app["store"]
limit = _safe_int(request.query.get("limit", "30"), 30, max_val=500)
scans = await asyncio.to_thread(store.get_recent_scans, limit)
return _json({"scans": scans, "count": len(scans)})
async def handle_scan_chart(request: web.Request) -> web.Response:
"""GET /api/scan-chart?days=30 - Scan history chart data."""
store = request.app["store"]
days = _safe_int(request.query.get("days", "30"), 30, max_val=365)
data = await asyncio.to_thread(store.get_scan_chart_data, days)
return _json({"chart": data})
async def handle_quarantine(request: web.Request) -> web.Response:
"""GET /api/quarantine - Quarantine vault status."""
vault = request.app.get("vault")
if not vault:
return _json({"count": 0, "items": [], "total_size": 0})
def _get() -> dict:
items = vault.list_quarantined()
total_size = sum(
item.get("file_size", 0) or item.get("size", 0) for item in items
)
return {"count": len(items), "items": items[:20], "total_size": total_size}
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_signatures(request: web.Request) -> web.Response:
"""GET /api/signatures - Signature database stats."""
store = request.app["store"]
def _get() -> dict:
sig_stats = store.get_sig_stats()
config = request.app.get("config")
if config:
try:
from ayn_antivirus.signatures.db.hash_db import HashDatabase
from ayn_antivirus.signatures.db.ioc_db import IOCDatabase
hdb = HashDatabase(config.db_path)
hdb.initialize()
idb = IOCDatabase(config.db_path)
idb.initialize()
sig_stats["db_hash_count"] = hdb.count()
sig_stats["db_hash_stats"] = hdb.get_stats()
sig_stats["db_ioc_stats"] = idb.get_stats()
sig_stats["db_malicious_ips"] = len(idb.get_all_malicious_ips())
sig_stats["db_malicious_domains"] = len(
idb.get_all_malicious_domains()
)
hdb.close()
idb.close()
except Exception as exc:
sig_stats["db_error"] = str(exc)
return sig_stats
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_sig_updates(request: web.Request) -> web.Response:
"""GET /api/sig-updates?limit=20 - Recent signature update history."""
store = request.app["store"]
limit = _safe_int(request.query.get("limit", "20"), 20, max_val=200)
updates = await asyncio.to_thread(store.get_recent_sig_updates, limit)
return _json({"updates": updates, "count": len(updates)})
async def handle_definitions(request: web.Request) -> web.Response:
"""GET /api/definitions - Full virus definition database view.
Supports pagination (``page``, ``per_page``), search (``search``),
and type filtering (``type=hash|ip|domain|url``).
"""
store = request.app["store"]
config = request.app.get("config")
page = _safe_int(request.query.get("page", "1"), 1, max_val=10000)
per_page = _safe_int(request.query.get("per_page", "100"), 100, max_val=500)
search = request.query.get("search", "").strip()
filter_type = request.query.get("type", "").strip()
def _get() -> dict:
result: dict = {
"hashes": [],
"ips": [],
"domains": [],
"urls": [],
"total_hashes": 0,
"total_ips": 0,
"total_domains": 0,
"total_urls": 0,
"page": page,
"per_page": per_page,
"feeds": [],
"last_update": None,
}
if not config:
return result
try:
from ayn_antivirus.signatures.db.hash_db import HashDatabase
from ayn_antivirus.signatures.db.ioc_db import IOCDatabase
hdb = HashDatabase(config.db_path)
hdb.initialize()
idb = IOCDatabase(config.db_path)
idb.initialize()
offset = (page - 1) * per_page
conn = hdb.conn
# Hash definitions
if not filter_type or filter_type == "hash":
if search:
rows = conn.execute(
"SELECT hash, threat_name, threat_type, severity, source, "
"added_date, details FROM threats "
"WHERE threat_name LIKE ? "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(f"%{search}%", per_page, offset),
).fetchall()
else:
rows = conn.execute(
"SELECT hash, threat_name, threat_type, severity, source, "
"added_date, details FROM threats "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(per_page, offset),
).fetchall()
result["hashes"] = [dict(r) for r in rows]
result["total_hashes"] = hdb.count()
# IP definitions
ioc_conn = idb.conn
if not filter_type or filter_type == "ip":
if search:
rows = ioc_conn.execute(
"SELECT ip, threat_name, type, source, added_date "
"FROM ioc_ips "
"WHERE ip LIKE ? OR threat_name LIKE ? "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(f"%{search}%", f"%{search}%", per_page, offset),
).fetchall()
else:
rows = ioc_conn.execute(
"SELECT ip, threat_name, type, source, added_date "
"FROM ioc_ips "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(per_page, offset),
).fetchall()
result["ips"] = [dict(r) for r in rows]
result["total_ips"] = ioc_conn.execute(
"SELECT COUNT(*) FROM ioc_ips"
).fetchone()[0]
# Domain definitions
if not filter_type or filter_type == "domain":
if search:
rows = ioc_conn.execute(
"SELECT domain, threat_name, type, source, added_date "
"FROM ioc_domains "
"WHERE domain LIKE ? OR threat_name LIKE ? "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(f"%{search}%", f"%{search}%", per_page, offset),
).fetchall()
else:
rows = ioc_conn.execute(
"SELECT domain, threat_name, type, source, added_date "
"FROM ioc_domains "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(per_page, offset),
).fetchall()
result["domains"] = [dict(r) for r in rows]
result["total_domains"] = ioc_conn.execute(
"SELECT COUNT(*) FROM ioc_domains"
).fetchone()[0]
# URL definitions
if not filter_type or filter_type == "url":
if search:
rows = ioc_conn.execute(
"SELECT url, threat_name, type, source, added_date "
"FROM ioc_urls "
"WHERE url LIKE ? OR threat_name LIKE ? "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(f"%{search}%", f"%{search}%", per_page, offset),
).fetchall()
else:
rows = ioc_conn.execute(
"SELECT url, threat_name, type, source, added_date "
"FROM ioc_urls "
"ORDER BY added_date DESC LIMIT ? OFFSET ?",
(per_page, offset),
).fetchall()
result["urls"] = [dict(r) for r in rows]
result["total_urls"] = ioc_conn.execute(
"SELECT COUNT(*) FROM ioc_urls"
).fetchone()[0]
# Feed info
sig_updates = store.get_recent_sig_updates(20)
result["feeds"] = sig_updates
result["last_update"] = (
sig_updates[0]["timestamp"] if sig_updates else None
)
hdb.close()
idb.close()
except Exception as exc:
result["error"] = str(exc)
logger.error("Error fetching definitions: %s", exc)
return result
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_logs(request: web.Request) -> web.Response:
"""GET /api/logs?limit=20 - Recent activity logs."""
store = request.app["store"]
limit = _safe_int(request.query.get("limit", "20"), 20, max_val=500)
logs = await asyncio.to_thread(store.get_recent_logs, limit)
return _json({"logs": logs, "count": len(logs)})
async def handle_metrics_history(request: web.Request) -> web.Response:
"""GET /api/metrics-history?hours=1 - Metrics time series."""
store = request.app["store"]
hours = _safe_int(request.query.get("hours", "1"), 1, max_val=168)
data = await asyncio.to_thread(store.get_metrics_history, hours)
return _json({"metrics": data, "count": len(data)})
# ------------------------------------------------------------------
# Action handlers (trigger scans / updates)
# ------------------------------------------------------------------
async def handle_action_quick_scan(request: web.Request) -> web.Response:
"""POST /api/actions/quick-scan - Trigger a quick scan."""
store = request.app["store"]
store.log_activity("Quick scan triggered from dashboard", "INFO", "dashboard")
def _run() -> dict:
from ayn_antivirus.config import Config
from ayn_antivirus.core.engine import ScanEngine
config = request.app.get("config") or Config()
engine = ScanEngine(config)
result = engine.quick_scan()
store.record_scan(
scan_type="quick",
scan_path=",".join(config.scan_paths),
files_scanned=result.files_scanned,
files_skipped=result.files_skipped,
threats_found=len(result.threats),
duration=result.duration_seconds,
)
# Build detection list for AI analysis
raw_threats = []
for t in result.threats:
raw_threats.append({
"file_path": t.path,
"threat_name": t.threat_name,
"threat_type": _threat_type_str(t.threat_type),
"severity": _severity_str(t.severity),
"detector": t.detector_name,
"file_hash": t.file_hash or "",
"confidence": getattr(t, "confidence", 50),
})
# AI filters out false positives
verified = _ai_filter_threats(request.app, store, raw_threats)
vault = request.app.get("vault")
quarantined = 0
for t in verified:
ai_action = t.get("ai_action", "quarantine")
sev = t["severity"]
qid = ""
if ai_action in ("quarantine", "delete"):
qid = _auto_quarantine(store, vault, t["file_path"], t["threat_name"], sev)
action = "quarantined" if qid else ("monitoring" if ai_action == "monitor" else "detected")
details = t.get("ai_reason", "")
store.record_threat(
file_path=t["file_path"],
threat_name=t["threat_name"],
threat_type=t["threat_type"],
severity=sev,
detector=t["detector"],
file_hash=t.get("file_hash", ""),
action=action,
details=f"[AI: {t.get('ai_verdict','?')} {t.get('ai_confidence',0)}%] {details}",
)
if qid:
quarantined += 1
return {
"status": "completed",
"files_scanned": result.files_scanned,
"threats_found": len(verified),
"ai_dismissed": len(result.threats) - len(verified),
"quarantined": quarantined,
}
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Quick scan failed: %s", exc)
store.log_activity(f"Quick scan failed: {exc}", "ERROR", "dashboard")
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_full_scan(request: web.Request) -> web.Response:
"""POST /api/actions/full-scan - Trigger a full scan."""
store = request.app["store"]
store.log_activity("Full scan triggered from dashboard", "INFO", "dashboard")
def _run() -> dict:
from ayn_antivirus.config import Config
from ayn_antivirus.core.engine import ScanEngine
config = request.app.get("config") or Config()
engine = ScanEngine(config)
result = engine.full_scan()
file_result = result.file_scan
store.record_scan(
scan_type="full",
scan_path=",".join(config.scan_paths),
files_scanned=file_result.files_scanned,
files_skipped=file_result.files_skipped,
threats_found=len(file_result.threats),
duration=file_result.duration_seconds,
)
raw_threats = []
for t in file_result.threats:
raw_threats.append({
"file_path": t.path,
"threat_name": t.threat_name,
"threat_type": _threat_type_str(t.threat_type),
"severity": _severity_str(t.severity),
"detector": t.detector_name,
"file_hash": t.file_hash or "",
"confidence": getattr(t, "confidence", 50),
})
verified = _ai_filter_threats(request.app, store, raw_threats)
vault = request.app.get("vault")
quarantined = 0
for t in verified:
ai_action = t.get("ai_action", "quarantine")
sev = t["severity"]
qid = ""
if ai_action in ("quarantine", "delete"):
qid = _auto_quarantine(store, vault, t["file_path"], t["threat_name"], sev)
action = "quarantined" if qid else ("monitoring" if ai_action == "monitor" else "detected")
details = t.get("ai_reason", "")
store.record_threat(
file_path=t["file_path"],
threat_name=t["threat_name"],
threat_type=t["threat_type"],
severity=sev,
detector=t["detector"],
file_hash=t.get("file_hash", ""),
action=action,
details=f"[AI: {t.get('ai_verdict','?')} {t.get('ai_confidence',0)}%] {details}",
)
if qid:
quarantined += 1
return {
"status": "completed",
"total_threats": result.total_threats,
"ai_verified": len(verified),
"ai_dismissed": len(raw_threats) - len(verified),
"quarantined": quarantined,
}
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Full scan failed: %s", exc)
store.log_activity(f"Full scan failed: {exc}", "ERROR", "dashboard")
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_update_sigs(request: web.Request) -> web.Response:
"""POST /api/actions/update-sigs - Update all threat signature feeds."""
store = request.app["store"]
store.log_activity(
"Signature update triggered from dashboard", "INFO", "dashboard"
)
def _run() -> dict:
from ayn_antivirus.config import Config
from ayn_antivirus.signatures.manager import SignatureManager
config = request.app.get("config") or Config()
manager = SignatureManager(config)
summary = manager.update_all()
# summary = {"feeds": {name: stats}, "total_new": int, "errors": [...]}
for feed_name, feed_result in summary["feeds"].items():
if "error" in feed_result:
store.record_sig_update(
feed_name=feed_name,
status="error",
details=feed_result.get("error", ""),
)
else:
store.record_sig_update(
feed_name=feed_name,
hashes=feed_result.get("hashes", 0),
ips=feed_result.get("ips", 0),
domains=feed_result.get("domains", 0),
urls=feed_result.get("urls", 0),
status="success",
details=json.dumps(feed_result),
)
manager.close()
store.log_activity(
f"Signature update completed: {len(summary['feeds'])} feeds, "
f"{summary['total_new']} new entries",
"INFO",
"signatures",
)
return {
"status": "completed",
"feeds_updated": len(summary["feeds"]) - len(summary["errors"]),
"total_new": summary["total_new"],
"errors": summary["errors"],
}
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Signature update failed: %s", exc)
store.log_activity(
f"Signature update failed: {exc}", "ERROR", "signatures"
)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_update_feed(request: web.Request) -> web.Response:
"""POST /api/actions/update-feed - Update a single feed.
Body: ``{"feed": "malwarebazaar"}``
"""
store = request.app["store"]
try:
body = await request.json()
feed_name = body.get("feed", "")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not feed_name:
return _json({"error": "Missing 'feed' parameter"}, 400)
store.log_activity(
f"Single feed update triggered: {feed_name}", "INFO", "dashboard"
)
def _run() -> dict:
from ayn_antivirus.config import Config
from ayn_antivirus.signatures.manager import SignatureManager
config = request.app.get("config") or Config()
manager = SignatureManager(config)
result = manager.update_feed(feed_name)
store.record_sig_update(
feed_name=feed_name,
hashes=result.get("hashes", 0),
ips=result.get("ips", 0),
domains=result.get("domains", 0),
urls=result.get("urls", 0),
status="success",
details=json.dumps(result),
)
manager.close()
return {"status": "completed", "feed": feed_name, "result": result}
try:
data = await asyncio.to_thread(_run)
return _json(data)
except KeyError as exc:
return _json({"status": "error", "error": str(exc)}, 404)
except Exception as exc:
logger.error("Feed update failed for %s: %s", feed_name, exc)
return _json({"status": "error", "error": str(exc)}, 500)
# ------------------------------------------------------------------
# Threat action handlers (quarantine / delete / whitelist)
# ------------------------------------------------------------------
async def handle_action_quarantine(request: web.Request) -> web.Response:
"""POST /api/actions/quarantine — Move a file to encrypted quarantine vault.
Body: ``{"file_path": "/path/to/file", "threat_id": 5}``
"""
store = request.app["store"]
vault = request.app.get("vault")
if not vault:
return _json({"status": "error", "error": "Quarantine vault not available"}, 500)
try:
body = await request.json()
file_path = body.get("file_path", "").strip()
threat_id = body.get("threat_id")
threat_name = body.get("threat_name", "Unknown")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not file_path:
return _json({"error": "Missing 'file_path'"}, 400)
def _run() -> dict:
import os
if not os.path.exists(file_path):
return {"status": "error", "error": f"File not found: {file_path}"}
qid = vault.quarantine_file(
file_path=file_path,
threat_name=threat_name,
threat_type="detected",
severity="HIGH",
)
if threat_id:
store.conn.execute(
"UPDATE threat_log SET action_taken='quarantined' WHERE id=?",
(threat_id,),
)
store.conn.commit()
store.log_activity(
f"Quarantined: {file_path} ({threat_name}) -> {qid}",
"WARNING", "quarantine",
)
return {"status": "ok", "quarantine_id": qid, "file_path": file_path}
try:
data = await asyncio.to_thread(_run)
return _json(data, 200 if data.get("status") == "ok" else 400)
except Exception as exc:
logger.error("Quarantine failed: %s", exc)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_delete_threat(request: web.Request) -> web.Response:
"""POST /api/actions/delete-threat — Permanently delete a malicious file.
Body: ``{"file_path": "/path/to/file", "threat_id": 5}``
"""
store = request.app["store"]
try:
body = await request.json()
file_path = body.get("file_path", "").strip()
threat_id = body.get("threat_id")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not file_path:
return _json({"error": "Missing 'file_path'"}, 400)
def _run() -> dict:
import os
if not os.path.exists(file_path):
if threat_id:
store.conn.execute(
"UPDATE threat_log SET action_taken='deleted' WHERE id=?",
(threat_id,),
)
store.conn.commit()
return {"status": "ok", "message": "File already gone", "file_path": file_path}
os.remove(file_path)
if threat_id:
store.conn.execute(
"UPDATE threat_log SET action_taken='deleted' WHERE id=?",
(threat_id,),
)
store.conn.commit()
store.log_activity(
f"Deleted threat file: {file_path}", "WARNING", "action",
)
return {"status": "ok", "file_path": file_path}
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Delete failed: %s", exc)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_whitelist(request: web.Request) -> web.Response:
"""POST /api/actions/whitelist — Mark a threat as false positive.
Body: ``{"threat_id": 5}``
"""
store = request.app["store"]
try:
body = await request.json()
threat_id = body.get("threat_id")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not threat_id:
return _json({"error": "Missing 'threat_id'"}, 400)
def _run() -> dict:
row = store.conn.execute(
"SELECT file_path, threat_name, file_hash FROM threat_log WHERE id=?",
(threat_id,),
).fetchone()
if not row:
return {"status": "error", "error": "Threat not found"}
store.conn.execute(
"UPDATE threat_log SET action_taken='whitelisted' WHERE id=?",
(threat_id,),
)
store.conn.commit()
store.log_activity(
f"Whitelisted: {row['file_path']} ({row['threat_name']})",
"INFO", "action",
)
return {"status": "ok", "threat_id": threat_id}
try:
data = await asyncio.to_thread(_run)
return _json(data, 200 if data.get("status") == "ok" else 400)
except Exception as exc:
logger.error("Whitelist failed: %s", exc)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_ai_analyze(request: web.Request) -> web.Response:
"""POST /api/actions/ai-analyze — Run AI analysis on a specific threat.
Body: ``{"threat_id": 5}``
"""
store = request.app["store"]
try:
body = await request.json()
threat_id = body.get("threat_id")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not threat_id:
return _json({"error": "Missing 'threat_id'"}, 400)
def _run() -> dict:
row = store.conn.execute(
"SELECT * FROM threat_log WHERE id=?", (threat_id,),
).fetchone()
if not row:
return {"status": "error", "error": "Threat not found"}
ai = _get_ai_analyzer(request.app)
if not ai or not ai.available:
return {"status": "error", "error": "AI not configured. Set ANTHROPIC_API_KEY."}
r = dict(row)
verdict = ai.analyze(
file_path=r["file_path"],
threat_name=r["threat_name"],
threat_type=r["threat_type"],
severity=r["severity"],
detector=r["detector"],
)
store.conn.execute(
"UPDATE threat_log SET details=? WHERE id=?",
(f"[AI: {verdict.verdict} {verdict.confidence}%] {verdict.reason}", threat_id),
)
store.conn.commit()
store.log_activity(
f"AI analyzed #{threat_id}: {verdict.verdict}{verdict.reason}",
"INFO", "ai_analyzer",
)
return {
"status": "ok",
"verdict": verdict.verdict,
"confidence": verdict.confidence,
"reason": verdict.reason,
"recommended_action": verdict.recommended_action,
}
try:
data = await asyncio.to_thread(_run)
return _json(data, 200 if data.get("status") == "ok" else 400)
except Exception as exc:
logger.error("AI analysis failed: %s", exc)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_restore(request: web.Request) -> web.Response:
"""POST /api/actions/restore — Restore a quarantined file.
Body: ``{"file_path": "/original/path", "threat_id": 5}``
"""
store = request.app["store"]
vault = request.app.get("vault")
if not vault:
return _json({"status": "error", "error": "Vault not available"}, 500)
try:
body = await request.json()
file_path = body.get("file_path", "").strip()
threat_id = body.get("threat_id")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not file_path:
return _json({"error": "Missing 'file_path'"}, 400)
def _run() -> dict:
items = vault.list_quarantined()
qid = None
for item in items:
if item.get("original_path") == file_path:
qid = item.get("id")
break
if not qid:
return {"status": "error", "error": f"No quarantine entry for {file_path}"}
vault.restore_file(qid)
if threat_id:
store.conn.execute(
"UPDATE threat_log SET action_taken='restored' WHERE id=?",
(threat_id,),
)
store.conn.commit()
store.log_activity(
f"Restored from quarantine: {file_path}", "WARNING", "quarantine",
)
return {"status": "ok", "file_path": file_path}
try:
data = await asyncio.to_thread(_run)
return _json(data, 200 if data.get("status") == "ok" else 400)
except Exception as exc:
logger.error("Restore failed: %s", exc)
return _json({"status": "error", "error": str(exc)}, 500)
# ------------------------------------------------------------------
# Container endpoints
# ------------------------------------------------------------------
async def handle_containers(request: web.Request) -> web.Response:
"""GET /api/containers - List all containers across runtimes."""
def _get() -> dict:
from ayn_antivirus.scanners.container_scanner import ContainerScanner
scanner = ContainerScanner()
containers = scanner.list_containers(
runtime="all", include_stopped=True,
)
return {
"containers": [c.to_dict() for c in containers],
"count": len(containers),
"runtimes": scanner.available_runtimes,
}
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_container_scan_results(request: web.Request) -> web.Response:
"""GET /api/container-scan - Recent container scan results from store."""
store = request.app["store"]
def _get() -> dict:
scans = store.conn.execute(
"SELECT * FROM scan_history "
"WHERE scan_type LIKE 'container%' "
"ORDER BY id DESC LIMIT 10",
).fetchall()
threats = store.conn.execute(
"SELECT * FROM threat_log WHERE "
"LOWER(threat_type) IN ('miner','misconfiguration','rootkit') "
"OR LOWER(detector) = 'container_scanner' "
"ORDER BY id DESC LIMIT 50",
).fetchall()
return {
"scans": [dict(r) for r in scans],
"threats": [dict(t) for t in threats],
}
data = await asyncio.to_thread(_get)
return _json(data)
async def handle_action_scan_containers(request: web.Request) -> web.Response:
"""POST /api/actions/scan-containers - Scan all containers."""
store = request.app["store"]
store.log_activity(
"Container scan triggered from dashboard", "INFO", "dashboard",
)
def _run() -> dict:
from ayn_antivirus.scanners.container_scanner import ContainerScanner
scanner = ContainerScanner()
result = scanner.scan("all")
store.record_scan(
scan_type="container-full",
scan_path="all-containers",
files_scanned=result.containers_scanned,
files_skipped=0,
threats_found=len(result.threats),
duration=result.duration_seconds,
status=(
"completed"
if not result.errors
else "completed_with_errors"
),
)
for t in result.threats:
store.record_threat(
file_path=t.file_path or f"container:{t.container_name}",
threat_name=t.threat_name,
threat_type=t.threat_type,
severity=t.severity,
detector="container_scanner",
file_hash="",
action="detected",
details=f"[{t.runtime}] {t.container_name}: {t.details}",
)
store.log_activity(
f"Container scan complete: {result.containers_found} found, "
f"{result.containers_scanned} scanned, "
f"{len(result.threats)} threats",
"INFO",
"container_scanner",
)
return result.to_dict()
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Container scan failed: %s", exc)
store.log_activity(
f"Container scan failed: {exc}", "ERROR", "container_scanner",
)
return _json({"status": "error", "error": str(exc)}, 500)
async def handle_action_scan_single_container(
request: web.Request,
) -> web.Response:
"""POST /api/actions/scan-container - Scan a single container.
Body: ``{"container_id": "abc123"}``
"""
store = request.app["store"]
try:
body = await request.json()
container_id = body.get("container_id", "")
except Exception:
return _json({"error": "Invalid JSON body"}, 400)
if not container_id:
return _json({"error": "Missing 'container_id'"}, 400)
store.log_activity(
f"Single container scan: {container_id}", "INFO", "dashboard",
)
def _run() -> dict:
from ayn_antivirus.scanners.container_scanner import ContainerScanner
scanner = ContainerScanner()
result = scanner.scan_container(container_id)
store.record_scan(
scan_type="container-single",
scan_path=container_id,
files_scanned=result.containers_scanned,
files_skipped=0,
threats_found=len(result.threats),
duration=result.duration_seconds,
)
for t in result.threats:
store.record_threat(
file_path=t.file_path or f"container:{t.container_name}",
threat_name=t.threat_name,
threat_type=t.threat_type,
severity=t.severity,
detector="container_scanner",
details=f"[{t.runtime}] {t.container_name}: {t.details}",
)
return result.to_dict()
try:
data = await asyncio.to_thread(_run)
return _json(data)
except Exception as exc:
logger.error("Container scan failed for %s: %s", container_id, exc)
return _json({"status": "error", "error": str(exc)}, 500)