from fastapi import Depends, FastAPI, HTTPException from app.adapters.iris import IrisAdapter from app.adapters.pagerduty import PagerDutyAdapter from app.adapters.shuffle import ShuffleAdapter from app.adapters.wazuh import WazuhAdapter from app.config import settings from app.db import init_schema from app.models import ( ActionCreateIncidentRequest, ApiResponse, 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") 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, ) 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)) @app.on_event("startup") async def startup() -> None: init_schema() repo.ensure_policy() @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.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})