| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- import asyncio
- import logging
- from datetime import datetime, timezone
- from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
- from app.adapters.abuseipdb import AbuseIpdbAdapter
- from app.adapters.iris import IrisAdapter
- from app.adapters.pagerduty import PagerDutyAdapter
- from app.adapters.shuffle import ShuffleAdapter
- from app.adapters.virustotal import VirusTotalAdapter
- from app.adapters.wazuh import WazuhAdapter
- from app.config import settings
- from app.db import init_schema
- from app.models import (
- ActionCreateIncidentRequest,
- ApiResponse,
- IocEnrichRequest,
- IocEvaluateRequest,
- IrisTicketCreateRequest,
- ShuffleLoginRequest,
- ShuffleProxyRequest,
- TriggerShuffleRequest,
- WazuhIngestRequest,
- )
- from app.repositories.mvp_repo import MvpRepository
- from app.routes.mvp import build_mvp_router
- from app.security import require_internal_api_key
- from app.services.mvp_service import MvpService
- app = FastAPI(title=settings.app_name, version="0.1.0")
- logger = logging.getLogger(__name__)
- wazuh_adapter = WazuhAdapter(
- base_url=settings.wazuh_base_url,
- username=settings.wazuh_username,
- password=settings.wazuh_password,
- indexer_url=settings.wazuh_indexer_url,
- indexer_username=settings.wazuh_indexer_username,
- indexer_password=settings.wazuh_indexer_password,
- )
- shuffle_adapter = ShuffleAdapter(
- base_url=settings.shuffle_base_url,
- api_key=settings.shuffle_api_key,
- )
- pagerduty_adapter = PagerDutyAdapter(
- base_url=settings.pagerduty_base_url,
- api_key=settings.pagerduty_api_key,
- )
- iris_adapter = IrisAdapter(
- base_url=settings.iris_base_url,
- api_key=settings.iris_api_key,
- )
- virustotal_adapter = VirusTotalAdapter(
- base_url=settings.virustotal_base_url,
- api_key=settings.virustotal_api_key,
- )
- abuseipdb_adapter = AbuseIpdbAdapter(
- base_url=settings.abuseipdb_base_url,
- api_key=settings.abuseipdb_api_key,
- )
- repo = MvpRepository()
- mvp_service = MvpService(
- repo=repo,
- wazuh_adapter=wazuh_adapter,
- shuffle_adapter=shuffle_adapter,
- iris_adapter=iris_adapter,
- pagerduty_adapter=pagerduty_adapter,
- )
- app.include_router(build_mvp_router(mvp_service, require_internal_api_key))
- async def _wazuh_auto_sync_loop() -> None:
- interval = max(5, int(settings.wazuh_auto_sync_interval_seconds))
- while True:
- started_at = datetime.now(timezone.utc).isoformat()
- try:
- app.state.wazuh_auto_sync_state["running"] = True
- app.state.wazuh_auto_sync_state["last_started_at"] = started_at
- result = await mvp_service.sync_wazuh_alerts(
- query=settings.wazuh_auto_sync_query,
- limit=settings.wazuh_auto_sync_limit,
- minutes=settings.wazuh_auto_sync_minutes,
- )
- app.state.wazuh_auto_sync_state["last_status"] = "ok"
- app.state.wazuh_auto_sync_state["last_result"] = result
- app.state.wazuh_auto_sync_state["last_finished_at"] = datetime.now(timezone.utc).isoformat()
- logger.info(
- "wazuh auto-sync processed=%s ingested=%s skipped=%s failed=%s ioc_evaluated=%s ioc_matched=%s ioc_rejected=%s",
- result.get("processed", 0),
- result.get("ingested", 0),
- result.get("skipped_existing", 0),
- result.get("failed", 0),
- result.get("ioc_evaluated", 0),
- result.get("ioc_matched", 0),
- result.get("ioc_rejected", 0),
- )
- except Exception as exc:
- app.state.wazuh_auto_sync_state["last_status"] = "error"
- app.state.wazuh_auto_sync_state["last_error"] = str(exc)
- app.state.wazuh_auto_sync_state["last_finished_at"] = datetime.now(timezone.utc).isoformat()
- logger.exception("wazuh auto-sync failed: %s", exc)
- finally:
- app.state.wazuh_auto_sync_state["running"] = False
- await asyncio.sleep(interval)
- @app.on_event("startup")
- async def startup() -> None:
- init_schema()
- repo.ensure_policy()
- app.state.wazuh_auto_sync_state = {
- "running": False,
- "last_status": None,
- "last_started_at": None,
- "last_finished_at": None,
- "last_error": None,
- "last_result": None,
- }
- if settings.wazuh_auto_sync_enabled:
- app.state.wazuh_auto_sync_task = asyncio.create_task(_wazuh_auto_sync_loop())
- logger.info(
- "wazuh auto-sync enabled interval=%ss limit=%s minutes=%s query=%s",
- settings.wazuh_auto_sync_interval_seconds,
- settings.wazuh_auto_sync_limit,
- settings.wazuh_auto_sync_minutes,
- settings.wazuh_auto_sync_query,
- )
- @app.on_event("shutdown")
- async def shutdown() -> None:
- task = getattr(app.state, "wazuh_auto_sync_task", None)
- if task:
- task.cancel()
- try:
- await task
- except asyncio.CancelledError:
- pass
- @app.get("/health", response_model=ApiResponse)
- async def health() -> ApiResponse:
- return ApiResponse(
- data={
- "service": settings.app_name,
- "env": settings.app_env,
- "targets": {
- "wazuh": settings.wazuh_base_url,
- "shuffle": settings.shuffle_base_url,
- "pagerduty": settings.pagerduty_base_url,
- "iris": settings.iris_base_url,
- },
- }
- )
- @app.post("/ingest/wazuh-alert", response_model=ApiResponse)
- async def ingest_wazuh_alert(payload: WazuhIngestRequest) -> ApiResponse:
- normalized = {
- "source": payload.source,
- "alert_id": payload.alert_id,
- "rule_id": payload.rule_id,
- "severity": payload.severity,
- "title": payload.title,
- "payload": payload.payload,
- }
- return ApiResponse(data={"normalized": normalized})
- @app.post("/action/create-incident", response_model=ApiResponse)
- async def create_incident(payload: ActionCreateIncidentRequest) -> ApiResponse:
- incident_payload = {
- "title": payload.title,
- "urgency": payload.severity,
- "incident_key": payload.dedupe_key,
- "body": payload.payload,
- "source": payload.source,
- }
- try:
- pd_result = await pagerduty_adapter.create_incident(incident_payload)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"PagerDuty call failed: {exc}") from exc
- return ApiResponse(data={"pagerduty": pd_result})
- @app.post("/action/trigger-shuffle", response_model=ApiResponse)
- async def trigger_shuffle(payload: TriggerShuffleRequest) -> ApiResponse:
- try:
- shuffle_result = await shuffle_adapter.trigger_workflow(
- workflow_id=payload.workflow_id,
- payload=payload.execution_argument,
- )
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": shuffle_result})
- @app.get("/shuffle/health", response_model=ApiResponse)
- async def shuffle_health() -> ApiResponse:
- try:
- result = await shuffle_adapter.health()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.get("/shuffle/auth-test", response_model=ApiResponse)
- async def shuffle_auth_test() -> ApiResponse:
- try:
- result = await shuffle_adapter.auth_test()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.post("/shuffle/login", response_model=ApiResponse)
- async def shuffle_login(payload: ShuffleLoginRequest) -> ApiResponse:
- try:
- result = await shuffle_adapter.login(payload.username, payload.password)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.post("/shuffle/generate-apikey", response_model=ApiResponse)
- async def shuffle_generate_apikey(payload: ShuffleLoginRequest | None = None) -> ApiResponse:
- username = payload.username if payload else settings.shuffle_username
- password = payload.password if payload else settings.shuffle_password
- if not username or not password:
- raise HTTPException(
- status_code=400,
- detail="Missing shuffle credentials. Provide username/password in body or set SHUFFLE_USERNAME and SHUFFLE_PASSWORD.",
- )
- try:
- result = await shuffle_adapter.generate_apikey_from_login(username, password)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.get("/shuffle/workflows", response_model=ApiResponse)
- async def shuffle_workflows() -> ApiResponse:
- try:
- result = await shuffle_adapter.list_workflows()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.get("/shuffle/workflows/{workflow_id}", response_model=ApiResponse)
- async def shuffle_workflow(workflow_id: str) -> ApiResponse:
- try:
- result = await shuffle_adapter.get_workflow(workflow_id)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.post("/shuffle/workflows/{workflow_id}/execute", response_model=ApiResponse)
- async def shuffle_workflow_execute(
- workflow_id: str, payload: dict[str, object]
- ) -> ApiResponse:
- try:
- result = await shuffle_adapter.trigger_workflow(workflow_id=workflow_id, payload=payload)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.get("/shuffle/apps", response_model=ApiResponse)
- async def shuffle_apps() -> ApiResponse:
- try:
- result = await shuffle_adapter.list_apps()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.post("/shuffle/proxy", response_model=ApiResponse)
- async def shuffle_proxy(payload: ShuffleProxyRequest) -> ApiResponse:
- path = payload.path if payload.path.startswith("/api/") else f"/api/v1/{payload.path.lstrip('/')}"
- try:
- result = await shuffle_adapter.proxy(
- method=payload.method,
- path=path,
- params=payload.params,
- payload=payload.payload,
- )
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Shuffle call failed: {exc}") from exc
- return ApiResponse(data={"shuffle": result})
- @app.post("/action/create-iris-case", response_model=ApiResponse)
- async def create_iris_case(payload: ActionCreateIncidentRequest) -> ApiResponse:
- # IRIS v2 expects case_name, case_description, case_customer, case_soc_id.
- case_payload = {
- "case_name": payload.title,
- "case_description": payload.payload.get("description", "Created by soc-integrator"),
- "case_customer": payload.payload.get("case_customer", settings.iris_default_customer_id),
- "case_soc_id": payload.payload.get("case_soc_id", settings.iris_default_soc_id),
- }
- try:
- iris_result = await iris_adapter.create_case(case_payload)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"IRIS call failed: {exc}") from exc
- return ApiResponse(data={"iris": iris_result})
- @app.post("/iris/tickets", response_model=ApiResponse)
- async def iris_create_ticket(payload: IrisTicketCreateRequest) -> ApiResponse:
- case_payload = {
- "case_name": payload.title,
- "case_description": payload.description,
- "case_customer": payload.case_customer or settings.iris_default_customer_id,
- "case_soc_id": payload.case_soc_id or settings.iris_default_soc_id,
- }
- if payload.payload:
- case_payload.update(payload.payload)
- try:
- iris_result = await iris_adapter.create_case(case_payload)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"IRIS call failed: {exc}") from exc
- return ApiResponse(data={"iris": iris_result})
- @app.get("/iris/tickets", response_model=ApiResponse)
- async def iris_list_tickets(limit: int = 50, offset: int = 0) -> ApiResponse:
- try:
- iris_result = await iris_adapter.list_cases(limit=limit, offset=offset)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"IRIS call failed: {exc}") from exc
- return ApiResponse(data={"iris": iris_result})
- def _build_vt_ioc_result(
- vt: dict[str, object],
- ioc_type: str,
- ioc_value: str,
- malicious_threshold: int,
- suspicious_threshold: int,
- ) -> tuple[dict[str, object], bool, str, float]:
- stats = (
- (((vt.get("data") or {}).get("attributes") or {}).get("last_analysis_stats"))
- if isinstance(vt, dict)
- else None
- ) or {}
- malicious = int(stats.get("malicious", 0) or 0)
- suspicious = int(stats.get("suspicious", 0) or 0)
- harmless = int(stats.get("harmless", 0) or 0)
- undetected = int(stats.get("undetected", 0) or 0)
- total = malicious + suspicious + harmless + undetected
- confidence = 0.0 if total == 0 else round(((malicious + (0.5 * suspicious)) / total), 4)
- matched = (malicious >= malicious_threshold) or (suspicious >= suspicious_threshold)
- severity = "low"
- if malicious >= 5 or suspicious >= 10:
- severity = "critical"
- elif malicious >= 2 or suspicious >= 5:
- severity = "high"
- elif malicious >= 1 or suspicious >= 1:
- severity = "medium"
- reason = (
- f"virustotal_stats malicious={malicious} suspicious={suspicious} "
- f"thresholds(malicious>={malicious_threshold}, suspicious>={suspicious_threshold})"
- )
- result: dict[str, object] = {
- "ioc_type": ioc_type,
- "ioc_value": ioc_value,
- "matched": matched,
- "severity": severity,
- "confidence": confidence,
- "reason": reason,
- "providers": {
- "virustotal": {
- "stats": stats,
- }
- },
- "raw": {
- "virustotal": vt,
- },
- }
- return result, matched, severity, confidence
- def _build_abuseipdb_ioc_result(
- abuse: dict[str, object],
- ioc_value: str,
- confidence_threshold: int = 50,
- ) -> tuple[dict[str, object], bool, str, float]:
- data = ((abuse.get("data") if isinstance(abuse, dict) else None) or {}) if isinstance(abuse, dict) else {}
- score = int(data.get("abuseConfidenceScore", 0) or 0)
- total_reports = int(data.get("totalReports", 0) or 0)
- matched = score >= confidence_threshold
- severity = "low"
- if score >= 90:
- severity = "critical"
- elif score >= 70:
- severity = "high"
- elif score >= 30:
- severity = "medium"
- confidence = round(score / 100.0, 4)
- reason = f"abuseipdb score={score} totalReports={total_reports} threshold>={confidence_threshold}"
- result: dict[str, object] = {
- "ioc_type": "ip",
- "ioc_value": ioc_value,
- "matched": matched,
- "severity": severity,
- "confidence": confidence,
- "reason": reason,
- "providers": {"abuseipdb": {"score": score, "totalReports": total_reports, "raw": abuse}},
- }
- return result, matched, severity, confidence
- @app.post("/ioc/enrich", response_model=ApiResponse)
- async def ioc_enrich(payload: IocEnrichRequest) -> ApiResponse:
- providers = [p.lower().strip() for p in payload.providers]
- result: dict[str, object] = {
- "ioc_type": payload.ioc_type,
- "ioc_value": payload.ioc_value,
- "providers_requested": providers,
- "providers": {},
- }
- if "virustotal" in providers:
- try:
- vt = await virustotal_adapter.enrich_ioc(payload.ioc_type, payload.ioc_value)
- result["providers"] = {**(result.get("providers") or {}), "virustotal": vt}
- except Exception as exc:
- repo.add_ioc_trace(
- action="enrich",
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- providers=providers,
- request_payload=payload.model_dump(mode="json"),
- response_payload={},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"VirusTotal call failed: {exc}") from exc
- if "abuseipdb" in providers:
- if payload.ioc_type != "ip":
- result["providers"] = {
- **(result.get("providers") or {}),
- "abuseipdb": {"skipped": "AbuseIPDB currently supports ioc_type='ip' only"},
- }
- else:
- try:
- abuse = await abuseipdb_adapter.check_ip(payload.ioc_value)
- result["providers"] = {**(result.get("providers") or {}), "abuseipdb": abuse}
- except Exception as exc:
- repo.add_ioc_trace(
- action="enrich",
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- providers=providers,
- request_payload=payload.model_dump(mode="json"),
- response_payload={},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"AbuseIPDB call failed: {exc}") from exc
- repo.add_ioc_trace(
- action="enrich",
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- providers=providers,
- request_payload=payload.model_dump(mode="json"),
- response_payload=result,
- )
- return ApiResponse(data={"ioc": result})
- @app.post("/ioc/evaluate", response_model=ApiResponse)
- async def ioc_evaluate(payload: IocEvaluateRequest) -> ApiResponse:
- providers = [p.lower().strip() for p in payload.providers]
- supported = {"virustotal", "abuseipdb"}
- requested = [p for p in providers if p in supported]
- if not requested:
- raise HTTPException(status_code=400, detail="No supported provider requested. Use ['virustotal'] or ['abuseipdb'].")
- per_provider: dict[str, dict[str, object]] = {}
- errors: dict[str, str] = {}
- if "virustotal" in requested:
- try:
- vt = await virustotal_adapter.enrich_ioc(payload.ioc_type, payload.ioc_value)
- vt_result, _, _, _ = _build_vt_ioc_result(
- vt=vt,
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- malicious_threshold=payload.malicious_threshold,
- suspicious_threshold=payload.suspicious_threshold,
- )
- per_provider["virustotal"] = vt_result
- except Exception as exc:
- errors["virustotal"] = str(exc)
- if "abuseipdb" in requested:
- if payload.ioc_type != "ip":
- errors["abuseipdb"] = "AbuseIPDB supports ioc_type='ip' only"
- else:
- try:
- abuse = await abuseipdb_adapter.check_ip(payload.ioc_value)
- abuse_result, _, _, _ = _build_abuseipdb_ioc_result(
- abuse=abuse,
- ioc_value=payload.ioc_value,
- confidence_threshold=50,
- )
- per_provider["abuseipdb"] = abuse_result
- except Exception as exc:
- errors["abuseipdb"] = str(exc)
- if not per_provider:
- repo.add_ioc_trace(
- action="evaluate",
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- providers=requested,
- request_payload=payload.model_dump(mode="json"),
- response_payload={},
- error=str(errors),
- )
- raise HTTPException(status_code=502, detail=f"Provider evaluation failed: {errors}")
- # aggregate decision (max confidence/severity, matched if any provider matched)
- order = {"low": 1, "medium": 2, "high": 3, "critical": 4}
- matched = any(bool(r.get("matched")) for r in per_provider.values())
- confidence = max(float(r.get("confidence", 0.0) or 0.0) for r in per_provider.values())
- severity = max((str(r.get("severity", "low")) for r in per_provider.values()), key=lambda x: order.get(x, 1))
- reason_parts = [f"{name}:{res.get('reason','')}" for name, res in per_provider.items()]
- if errors:
- reason_parts.append(f"errors={errors}")
- ioc_result = {
- "ioc_type": payload.ioc_type,
- "ioc_value": payload.ioc_value,
- "matched": matched,
- "severity": severity,
- "confidence": round(confidence, 4),
- "reason": " | ".join(reason_parts),
- "providers": per_provider,
- }
- repo.add_ioc_trace(
- action="evaluate",
- ioc_type=payload.ioc_type,
- ioc_value=payload.ioc_value,
- providers=providers,
- request_payload=payload.model_dump(mode="json"),
- response_payload=ioc_result,
- matched=matched,
- severity=severity,
- confidence=float(ioc_result["confidence"]),
- )
- return ApiResponse(data={"ioc": ioc_result})
- @app.post("/ioc/upload-file", response_model=ApiResponse)
- async def ioc_upload_file(file: UploadFile = File(...)) -> ApiResponse:
- content = await file.read()
- if not content:
- raise HTTPException(status_code=400, detail="Uploaded file is empty")
- try:
- vt_upload = await virustotal_adapter.upload_file(file.filename or "upload.bin", content)
- except Exception as exc:
- repo.add_ioc_trace(
- action="upload_file",
- ioc_type="hash",
- ioc_value=file.filename or "<unknown>",
- providers=["virustotal"],
- request_payload={"filename": file.filename, "size": len(content)},
- response_payload={},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"VirusTotal upload failed: {exc}") from exc
- repo.add_ioc_trace(
- action="upload_file",
- ioc_type="hash",
- ioc_value=file.filename or "<unknown>",
- providers=["virustotal"],
- request_payload={"filename": file.filename, "size": len(content)},
- response_payload=vt_upload if isinstance(vt_upload, dict) else {"raw": str(vt_upload)},
- )
- return ApiResponse(data={"virustotal": vt_upload})
- @app.get("/ioc/analysis/{analysis_id}", response_model=ApiResponse)
- async def ioc_get_analysis(analysis_id: str) -> ApiResponse:
- try:
- vt_analysis = await virustotal_adapter.get_analysis(analysis_id)
- except Exception as exc:
- repo.add_ioc_trace(
- action="analysis",
- ioc_type="hash",
- ioc_value=analysis_id,
- providers=["virustotal"],
- request_payload={"analysis_id": analysis_id},
- response_payload={},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"VirusTotal analysis fetch failed: {exc}") from exc
- repo.add_ioc_trace(
- action="analysis",
- ioc_type="hash",
- ioc_value=analysis_id,
- providers=["virustotal"],
- request_payload={"analysis_id": analysis_id},
- response_payload=vt_analysis if isinstance(vt_analysis, dict) else {"raw": str(vt_analysis)},
- )
- return ApiResponse(data={"virustotal": vt_analysis})
- @app.post("/ioc/evaluate-file", response_model=ApiResponse)
- async def ioc_evaluate_file(
- file: UploadFile = File(...),
- malicious_threshold: int = 1,
- suspicious_threshold: int = 3,
- poll_timeout_seconds: int = 30,
- poll_interval_seconds: int = 2,
- ) -> ApiResponse:
- content = await file.read()
- if not content:
- raise HTTPException(status_code=400, detail="Uploaded file is empty")
- try:
- vt_upload = await virustotal_adapter.upload_file(file.filename or "upload.bin", content)
- except Exception as exc:
- repo.add_ioc_trace(
- action="evaluate_file",
- ioc_type="hash",
- ioc_value=file.filename or "<unknown>",
- providers=["virustotal"],
- request_payload={"filename": file.filename, "size": len(content)},
- response_payload={},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"VirusTotal upload failed: {exc}") from exc
- analysis_id = (
- (((vt_upload.get("data") or {}).get("id")) if isinstance(vt_upload, dict) else None)
- or ""
- )
- if not analysis_id:
- raise HTTPException(status_code=502, detail="VirusTotal upload response missing analysis ID")
- timeout = max(1, poll_timeout_seconds)
- interval = max(1, poll_interval_seconds)
- elapsed = 0
- analysis: dict[str, object] = {}
- while elapsed <= timeout:
- analysis = await virustotal_adapter.get_analysis(analysis_id)
- status = (
- (((analysis.get("data") or {}).get("attributes") or {}).get("status"))
- if isinstance(analysis, dict)
- else None
- )
- if status == "completed":
- break
- await asyncio.sleep(interval)
- elapsed += interval
- sha256 = (
- (((analysis.get("meta") or {}).get("file_info") or {}).get("sha256"))
- if isinstance(analysis, dict)
- else None
- )
- if not sha256:
- raise HTTPException(status_code=502, detail="VirusTotal analysis did not return file hash yet")
- try:
- vt_file = await virustotal_adapter.enrich_ioc("hash", str(sha256))
- except Exception as exc:
- repo.add_ioc_trace(
- action="evaluate_file",
- ioc_type="hash",
- ioc_value=str(sha256),
- providers=["virustotal"],
- request_payload={"filename": file.filename, "analysis_id": analysis_id},
- response_payload={"upload": vt_upload, "analysis": analysis},
- error=str(exc),
- )
- raise HTTPException(status_code=502, detail=f"VirusTotal report fetch failed: {exc}") from exc
- ioc_result, matched, severity, confidence = _build_vt_ioc_result(
- vt=vt_file,
- ioc_type="hash",
- ioc_value=str(sha256),
- malicious_threshold=malicious_threshold,
- suspicious_threshold=suspicious_threshold,
- )
- ioc_result["analysis_id"] = analysis_id
- ioc_result["filename"] = file.filename
- repo.add_ioc_trace(
- action="evaluate_file",
- ioc_type="hash",
- ioc_value=str(sha256),
- providers=["virustotal"],
- request_payload={"filename": file.filename, "analysis_id": analysis_id},
- response_payload={
- "upload": vt_upload,
- "analysis": analysis,
- "ioc": ioc_result,
- },
- matched=matched,
- severity=severity,
- confidence=confidence,
- )
- return ApiResponse(data={"ioc": ioc_result, "analysis": analysis, "upload": vt_upload})
- @app.get("/ioc/history", response_model=ApiResponse)
- async def ioc_history(limit: int = 50, offset: int = 0) -> ApiResponse:
- return ApiResponse(data={"items": repo.list_ioc_trace(limit=limit, offset=offset)})
- @app.get("/sync/wazuh-version", response_model=ApiResponse)
- async def sync_wazuh_version() -> ApiResponse:
- try:
- wazuh_result = await wazuh_adapter.get_version()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh call failed: {exc}") from exc
- return ApiResponse(data={"wazuh": wazuh_result})
- @app.get("/wazuh/auth-test", response_model=ApiResponse)
- async def wazuh_auth_test() -> ApiResponse:
- try:
- result = await wazuh_adapter.auth_test()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh auth failed: {exc}") from exc
- return ApiResponse(data={"wazuh": result})
- @app.get("/wazuh/manager-info", response_model=ApiResponse)
- async def wazuh_manager_info() -> ApiResponse:
- try:
- result = await wazuh_adapter.get_manager_info()
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh call failed: {exc}") from exc
- return ApiResponse(data={"wazuh": result})
- @app.get("/wazuh/agents", response_model=ApiResponse)
- async def wazuh_agents(
- limit: int = 50,
- offset: int = 0,
- select: str | None = None,
- ) -> ApiResponse:
- try:
- result = await wazuh_adapter.list_agents(limit=limit, offset=offset, select=select)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh call failed: {exc}") from exc
- return ApiResponse(data={"wazuh": result})
- @app.get("/wazuh/alerts", response_model=ApiResponse)
- async def wazuh_alerts(
- limit: int = 50,
- offset: int = 0,
- q: str | None = None,
- sort: str | None = None,
- ) -> ApiResponse:
- try:
- # In this Wazuh build, API alerts are exposed via manager logs.
- result = await wazuh_adapter.list_manager_logs(
- limit=limit, offset=offset, q=q, sort=sort
- )
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh call failed: {exc}") from exc
- return ApiResponse(data={"wazuh": result})
- @app.get("/wazuh/manager-logs", response_model=ApiResponse)
- async def wazuh_manager_logs(
- limit: int = 50,
- offset: int = 0,
- q: str | None = None,
- sort: str | None = None,
- ) -> ApiResponse:
- try:
- result = await wazuh_adapter.list_manager_logs(
- limit=limit, offset=offset, q=q, sort=sort
- )
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh call failed: {exc}") from exc
- return ApiResponse(data={"wazuh": result})
- @app.post("/wazuh/sync-to-mvp", response_model=ApiResponse, dependencies=[Depends(require_internal_api_key)])
- async def wazuh_sync_to_mvp(
- limit: int = 50,
- minutes: int = 120,
- q: str = "soc_mvp_test=true OR event_type:*",
- ) -> ApiResponse:
- try:
- result = await mvp_service.sync_wazuh_alerts(query=q, limit=limit, minutes=minutes)
- except Exception as exc:
- raise HTTPException(status_code=502, detail=f"Wazuh sync failed: {exc}") from exc
- return ApiResponse(data={"sync": result})
- @app.get("/wazuh/auto-sync/status", response_model=ApiResponse)
- async def wazuh_auto_sync_status() -> ApiResponse:
- state = getattr(app.state, "wazuh_auto_sync_state", {})
- task = getattr(app.state, "wazuh_auto_sync_task", None)
- return ApiResponse(
- data={
- "enabled": settings.wazuh_auto_sync_enabled,
- "task_running": bool(task and not task.done()),
- "settings": {
- "interval_seconds": settings.wazuh_auto_sync_interval_seconds,
- "limit": settings.wazuh_auto_sync_limit,
- "minutes": settings.wazuh_auto_sync_minutes,
- "query": settings.wazuh_auto_sync_query,
- },
- "state": state,
- }
- )
|