feat: implement Phase 4 enhancements
Some checks failed
CI / lint-and-test (push) Has been cancelled

- Migrate frontend to Alpine.js for reactive state management
- Add source health dashboard in UI and /api/source-health endpoint
- Add event tagging (PATCH /api/events/{id}/tags) and commenting (POST /api/events/{id}/comments)
- Add CSV/JSON export from the UI
- Add rule-based alerting engine (rules.py) with CRUD endpoints (/api/rules)
- Add SIEM export via webhook (siem.py)
- Add AOC audit trail middleware logging all mutations to aoc_audit collection
- Update config with SIEM_ENABLED, SIEM_WEBHOOK_URL, ALERTS_ENABLED
- Add tests for rules engine, tags, comments, and source health
This commit is contained in:
2026-04-14 15:38:39 +02:00
parent b0198012eb
commit b35cac42e0
18 changed files with 869 additions and 370 deletions

View File

@@ -1,10 +1,18 @@
import base64
import re
from datetime import UTC, datetime
from audit_trail import log_action
from auth import require_auth
from bson import ObjectId
from database import events_collection
from fastapi import APIRouter, Depends, HTTPException, Query
from models.api import FilterOptionsResponse, PaginatedEventResponse
from models.api import (
CommentAddRequest,
FilterOptionsResponse,
PaginatedEventResponse,
TagsUpdateRequest,
)
router = APIRouter(dependencies=[Depends(require_auth)])
@@ -34,6 +42,7 @@ def list_events(
search: str | None = None,
cursor: str | None = None,
page_size: int = Query(default=50, ge=1, le=500),
user: dict = Depends(require_auth),
):
filters = []
@@ -85,7 +94,7 @@ def list_events(
{
"$or": [
{"timestamp": {"$lt": cursor_ts}},
{"timestamp": cursor_ts, "_id": {"$lt": cursor_oid}},
{"timestamp": cursor_ts, "_id": {"$lt": ObjectId(cursor_oid)}},
]
}
)
@@ -112,6 +121,12 @@ def list_events(
for e in events:
e["_id"] = str(e["_id"])
log_action("list_events", "/api/events", {"filters": {k: v for k, v in {
"service": service, "actor": actor, "operation": operation, "result": result,
"start": start, "end": end, "search": search, "cursor": cursor, "page_size": page_size,
}.items() if v is not None}}, user.get("sub", "anonymous"))
return {
"items": events,
"total": total,
@@ -122,9 +137,6 @@ def list_events(
@router.get("/filter-options", response_model=FilterOptionsResponse)
def filter_options(limit: int = Query(default=200, ge=1, le=1000)):
"""
Provide distinct values for UI filters (best-effort, capped).
"""
safe_limit = max(1, min(limit, 1000))
try:
services = sorted(events_collection.distinct("service"))[:safe_limit]
@@ -144,3 +156,34 @@ def filter_options(limit: int = Query(default=200, ge=1, le=1000)):
"actor_upns": actor_upns,
"devices": devices,
}
@router.patch("/events/{event_id}/tags")
def update_tags(
event_id: str,
body: TagsUpdateRequest,
user: dict = Depends(require_auth),
):
result = events_collection.update_one({"id": event_id}, {"$set": {"tags": body.tags}})
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Event not found")
log_action("update_tags", f"/api/events/{event_id}/tags", {"tags": body.tags}, user.get("sub", "anonymous"))
return {"tags": body.tags}
@router.post("/events/{event_id}/comments")
def add_comment(
event_id: str,
body: CommentAddRequest,
user: dict = Depends(require_auth),
):
comment = {
"text": body.text,
"author": user.get("sub", "anonymous"),
"timestamp": datetime.now(UTC).isoformat(),
}
result = events_collection.update_one({"id": event_id}, {"$push": {"comments": comment}})
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Event not found")
log_action("add_comment", f"/api/events/{event_id}/comments", {"text": body.text}, user.get("sub", "anonymous"))
return comment