Spaces:
Sleeping
Sleeping
Serve Next.js SOC dashboard at /ui with FastAPI redirect from /.
Browse files- .dockerignore +1 -2
- .gitignore +1 -2
- Dockerfile +13 -2
- README.md +16 -0
- backend/app/main.py +13 -3
- frontend/next.config.ts +11 -0
- frontend/src/app/page.tsx +18 -4
.dockerignore
CHANGED
|
@@ -11,7 +11,6 @@ chroma_data
|
|
| 11 |
.env
|
| 12 |
frontend/node_modules
|
| 13 |
frontend/.next
|
|
|
|
| 14 |
*.log
|
| 15 |
-
import_profile*.txt
|
| 16 |
-
import_p3.txt
|
| 17 |
.tmp_import.log
|
|
|
|
| 11 |
.env
|
| 12 |
frontend/node_modules
|
| 13 |
frontend/.next
|
| 14 |
+
frontend/out
|
| 15 |
*.log
|
|
|
|
|
|
|
| 16 |
.tmp_import.log
|
.gitignore
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
.venv/
|
| 2 |
-
.venv_py314/
|
| 3 |
__pycache__/
|
| 4 |
*.py[cod]
|
| 5 |
.pytest_cache/
|
|
@@ -8,7 +7,7 @@ __pycache__/
|
|
| 8 |
chroma_data/
|
| 9 |
.env
|
| 10 |
frontend/.next/
|
|
|
|
| 11 |
frontend/node_modules/
|
| 12 |
dist/
|
| 13 |
*.log
|
| 14 |
-
!demo_logs/auth_demo.log
|
|
|
|
| 1 |
.venv/
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
| 4 |
.pytest_cache/
|
|
|
|
| 7 |
chroma_data/
|
| 8 |
.env
|
| 9 |
frontend/.next/
|
| 10 |
+
frontend/out/
|
| 11 |
frontend/node_modules/
|
| 12 |
dist/
|
| 13 |
*.log
|
|
|
Dockerfile
CHANGED
|
@@ -1,5 +1,15 @@
|
|
| 1 |
-
# Hugging Face Spaces (Docker
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
FROM python:3.12-slim
|
| 4 |
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
@@ -22,6 +32,7 @@ COPY requirements.txt /app/requirements.txt
|
|
| 22 |
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 23 |
|
| 24 |
COPY . /app
|
|
|
|
| 25 |
|
| 26 |
RUN mkdir -p /app/demo_logs
|
| 27 |
|
|
|
|
| 1 |
+
# Hugging Face Spaces (Docker): API + static Next.js dashboard at /ui/
|
| 2 |
+
FROM node:22-alpine AS frontend_build
|
| 3 |
+
WORKDIR /build/frontend
|
| 4 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 5 |
+
RUN npm ci
|
| 6 |
+
COPY frontend ./
|
| 7 |
+
ENV NEXT_TELEMETRY_DISABLED=1 \
|
| 8 |
+
NEXT_STATIC_EXPORT=1 \
|
| 9 |
+
NEXT_PUBLIC_BASE_PATH=/ui \
|
| 10 |
+
NEXT_PUBLIC_API_URL=
|
| 11 |
+
RUN npm run build
|
| 12 |
+
|
| 13 |
FROM python:3.12-slim
|
| 14 |
|
| 15 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
|
|
| 32 |
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 33 |
|
| 34 |
COPY . /app
|
| 35 |
+
COPY --from=frontend_build /build/frontend/out /app/frontend/out
|
| 36 |
|
| 37 |
RUN mkdir -p /app/demo_logs
|
| 38 |
|
README.md
CHANGED
|
@@ -14,6 +14,22 @@ short_description: SentinelAI — Autonomous Multi-Agent AI SOC
|
|
| 14 |
|
| 15 |
---
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# SentinelAI — Autonomous Multi-Agent AI SOC
|
| 18 |
|
| 19 |
SentinelAI is a hackathon-grade, production-shaped **autonomous Security Operations Center**. It continuously ingests telemetry through collector agents, normalizes and enriches events, runs multi-modal detection (rules + heuristics + optional LLM reasoning on AMD ROCm), correlates attack chains, scores risk, drafts analyst narratives, emits remediation, and fans out alerts—while a **Next.js 15** command deck visualizes live operations.
|
|
|
|
| 14 |
|
| 15 |
---
|
| 16 |
|
| 17 |
+
---
|
| 18 |
+
title: SentinelAI
|
| 19 |
+
emoji: 🏃
|
| 20 |
+
colorFrom: red
|
| 21 |
+
colorTo: pink
|
| 22 |
+
sdk: docker
|
| 23 |
+
app_port: 7860
|
| 24 |
+
pinned: false
|
| 25 |
+
license: apache-2.0
|
| 26 |
+
short_description: SentinelAI — Autonomous Multi-Agent AI SOC
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
**This Hugging Face Space** serves the SOC dashboard at **`/ui/`** (static Next.js behind FastAPI). Open the Space URL in a browser and you are redirected from `/` to the deck; API docs stay at **`/docs`**. The container uses `SKIP_DB=1` (no bundled PostgreSQL/Redis).
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
# SentinelAI — Autonomous Multi-Agent AI SOC
|
| 34 |
|
| 35 |
SentinelAI is a hackathon-grade, production-shaped **autonomous Security Operations Center**. It continuously ingests telemetry through collector agents, normalizes and enriches events, runs multi-modal detection (rules + heuristics + optional LLM reasoning on AMD ROCm), correlates attack chains, scores risk, drafts analyst narratives, emits remediation, and fans out alerts—while a **Next.js 15** command deck visualizes live operations.
|
backend/app/main.py
CHANGED
|
@@ -12,8 +12,10 @@ import threading
|
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Annotated, Any
|
| 14 |
|
| 15 |
-
from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
|
| 16 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
| 17 |
|
| 18 |
ROOT = Path(__file__).resolve().parents[2]
|
| 19 |
if str(ROOT) not in sys.path:
|
|
@@ -174,6 +176,10 @@ app.add_middleware(
|
|
| 174 |
allow_headers=["*"],
|
| 175 |
)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
async def get_session():
|
| 179 |
if os.getenv("SKIP_DB", "").lower() in {"1", "true", "yes"}:
|
|
@@ -280,10 +286,14 @@ async def replay_buffer():
|
|
| 280 |
|
| 281 |
|
| 282 |
@app.get("/")
|
| 283 |
-
async def root():
|
| 284 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 285 |
return {
|
| 286 |
"service": "SentinelAI SOC API",
|
|
|
|
| 287 |
"docs": "/docs",
|
| 288 |
"health": "/health",
|
| 289 |
"openapi_json": "/openapi.json",
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Annotated, Any
|
| 14 |
|
| 15 |
+
from fastapi import Depends, FastAPI, Request, WebSocket, WebSocketDisconnect
|
| 16 |
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from fastapi.responses import RedirectResponse
|
| 18 |
+
from fastapi.staticfiles import StaticFiles
|
| 19 |
|
| 20 |
ROOT = Path(__file__).resolve().parents[2]
|
| 21 |
if str(ROOT) not in sys.path:
|
|
|
|
| 176 |
allow_headers=["*"],
|
| 177 |
)
|
| 178 |
|
| 179 |
+
_UI_STATIC = ROOT / "frontend" / "out"
|
| 180 |
+
if _UI_STATIC.is_dir():
|
| 181 |
+
app.mount("/ui", StaticFiles(directory=str(_UI_STATIC), html=True), name="ui")
|
| 182 |
+
|
| 183 |
|
| 184 |
async def get_session():
|
| 185 |
if os.getenv("SKIP_DB", "").lower() in {"1", "true", "yes"}:
|
|
|
|
| 286 |
|
| 287 |
|
| 288 |
@app.get("/")
|
| 289 |
+
async def root(request: Request):
|
| 290 |
+
"""Browsers get the Next dashboard at `/ui` when static export is baked in; API clients keep JSON."""
|
| 291 |
+
accept = request.headers.get("accept") or ""
|
| 292 |
+
if _UI_STATIC.is_dir() and accept.startswith("text/html"):
|
| 293 |
+
return RedirectResponse(url="/ui/", status_code=302)
|
| 294 |
return {
|
| 295 |
"service": "SentinelAI SOC API",
|
| 296 |
+
"dashboard": "/ui/",
|
| 297 |
"docs": "/docs",
|
| 298 |
"health": "/health",
|
| 299 |
"openapi_json": "/openapi.json",
|
frontend/next.config.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
import path from "path";
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const nextConfig: NextConfig = {
|
| 5 |
// Keeps output file tracing anchored to this app when multiple lockfiles exist on the machine.
|
| 6 |
outputFileTracingRoot: path.join(process.cwd()),
|
|
@@ -8,6 +12,13 @@ const nextConfig: NextConfig = {
|
|
| 8 |
eslint: {
|
| 9 |
ignoreDuringBuilds: true,
|
| 10 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
export default nextConfig;
|
|
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
import path from "path";
|
| 3 |
|
| 4 |
+
/** When `NEXT_STATIC_EXPORT=1`, emit `out/` for embedding behind FastAPI (e.g. Hugging Face Docker Space). */
|
| 5 |
+
const staticExport = process.env.NEXT_STATIC_EXPORT === "1";
|
| 6 |
+
const basePath = process.env.NEXT_PUBLIC_BASE_PATH?.trim() ?? "";
|
| 7 |
+
|
| 8 |
const nextConfig: NextConfig = {
|
| 9 |
// Keeps output file tracing anchored to this app when multiple lockfiles exist on the machine.
|
| 10 |
outputFileTracingRoot: path.join(process.cwd()),
|
|
|
|
| 12 |
eslint: {
|
| 13 |
ignoreDuringBuilds: true,
|
| 14 |
},
|
| 15 |
+
...(staticExport
|
| 16 |
+
? {
|
| 17 |
+
output: "export" as const,
|
| 18 |
+
images: { unoptimized: true },
|
| 19 |
+
}
|
| 20 |
+
: {}),
|
| 21 |
+
...(basePath ? { basePath } : {}),
|
| 22 |
};
|
| 23 |
|
| 24 |
export default nextConfig;
|
frontend/src/app/page.tsx
CHANGED
|
@@ -71,13 +71,27 @@ function normalizeApiOrigin(raw: string): string {
|
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
| 74 |
-
function apiBase() {
|
| 75 |
-
const raw = process.env.NEXT_PUBLIC_API_URL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return normalizeApiOrigin(raw);
|
| 77 |
}
|
| 78 |
|
| 79 |
-
function wsUrl() {
|
| 80 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
return base.replace(/^http/, "ws") + "/live-events";
|
| 82 |
}
|
| 83 |
|
|
|
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
| 74 |
+
function apiBase(): string {
|
| 75 |
+
const raw = process.env.NEXT_PUBLIC_API_URL;
|
| 76 |
+
if (raw === undefined || raw === "") {
|
| 77 |
+
if (typeof window !== "undefined") {
|
| 78 |
+
return window.location.origin;
|
| 79 |
+
}
|
| 80 |
+
return "";
|
| 81 |
+
}
|
| 82 |
return normalizeApiOrigin(raw);
|
| 83 |
}
|
| 84 |
|
| 85 |
+
function wsUrl(): string {
|
| 86 |
+
const raw = process.env.NEXT_PUBLIC_API_URL;
|
| 87 |
+
if (raw === undefined || raw === "") {
|
| 88 |
+
if (typeof window !== "undefined") {
|
| 89 |
+
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
| 90 |
+
return `${proto}//${window.location.host}/live-events`;
|
| 91 |
+
}
|
| 92 |
+
return "ws://127.0.0.1:8000/live-events";
|
| 93 |
+
}
|
| 94 |
+
const base = normalizeApiOrigin(raw);
|
| 95 |
return base.replace(/^http/, "ws") + "/live-events";
|
| 96 |
}
|
| 97 |
|