Add Vue 3 dashboard frontend with nginx reverse proxy, charts and LLM analysis UI
This commit is contained in:
@@ -1,21 +1,22 @@
|
|||||||
# Log Analyzer Backend
|
# Log Analyzer Backend + Frontend
|
||||||
|
|
||||||
CPU-LLM-gestütztes Backend zur Auswertung von Firewall- und Proxy-Logs. Parst Dateien, aggregiert Statistiken (Top-Hits, Quellen, Ziele, Ports, URLs) und nutzt ein lokales LLM (via Ollama) für die Analyse.
|
CPU-LLM-gestütztes Dashboard zur Auswertung von Firewall- und Proxy-Logs. Parst Dateien, zeigt übersichtliche Statistiken (Top-Hits, Quellen, Ziele, Ports, URLs, Zeitverlauf) und nutzt ein lokales LLM (via Ollama) für die Analyse.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **Log-Parsing:** Unterstützt iptables, pfSense, Cisco ASA, Squid, nginx
|
- **Log-Parsing:** Unterstützt iptables, pfSense, Cisco ASA, Squid, nginx
|
||||||
- **Statistiken:** Top Quellen, Ziele, Ports, URLs, Actions, Timeline
|
- **Statistiken:** Top Quellen, Ziele, Ports, URLs, Actions, Timeline
|
||||||
- **LLM-Analyse:** Zusammenfassung und Anomalie-Erkennung via lokalem Ollama-Modell
|
- **LLM-Analyse:** Zusammenfassung und Anomalie-Erkennung via lokalem Ollama-Modell
|
||||||
- **REST-API:** FastAPI mit automatischer OpenAPI-Doku unter `/docs`
|
- **REST-API:** FastAPI mit automatischer OpenAPI-Doku unter `/docs`
|
||||||
- **Docker-Compose:** Schneller Start mit Backend + nginx Reverse Proxy
|
- **Web-Dashboard:** Vue 3 + Chart.js SPA, erreichbar auf Port 80
|
||||||
|
- **Docker-Compose:** Ein Befehl zum Starten von Backend + Frontend
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
```
|
```
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
|
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
|
||||||
│ Client │──────▶│ nginx:80 │──────▶│ FastAPI Backend:8000 │
|
│ Browser │──────▶│ Frontend:80 │──────▶│ FastAPI Backend:8000 │
|
||||||
└──────────────┘ └──────────────┘ │ - SQLite (Volumen) │
|
└──────────────┘ │ (nginx + │ │ - SQLite (Volumen) │
|
||||||
│ - Log-Parser │
|
│ Vue SPA) │ │ - Log-Parser │
|
||||||
│ - Ollama-Client │
|
└──────────────┘ │ - Ollama-Client │
|
||||||
└───────────────────────┘
|
└───────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -29,7 +30,7 @@ CPU-LLM-gestütztes Backend zur Auswertung von Firewall- und Proxy-Logs. Parst D
|
|||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
- Docker + Docker Compose
|
- Docker + Docker Compose
|
||||||
- Laufende Ollama-Instanz auf dem Host (Port 11434) mit Modell `llava:7b`
|
- Laufende Ollama-Instanz auf dem Host (Port 11434) mit Modell `llava:7b`
|
||||||
- Ports: 8080 (nginx), 11434 (Ollama auf Host)
|
- Ports: 80 (Dashboard), 11434 (Ollama auf Host)
|
||||||
|
|
||||||
## Installation & Start
|
## Installation & Start
|
||||||
|
|
||||||
@@ -42,30 +43,38 @@ cd log-analyzer-backend
|
|||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Die API ist dann unter `http://localhost:8080/api` erreichbar.
|
Das Dashboard ist dann unter `http://localhost` erreichbar. Die API-Doku unter `http://localhost/docs`.
|
||||||
|
|
||||||
## Nutzung
|
## Nutzung
|
||||||
|
|
||||||
### Log-Datei hochladen
|
### Dashboard
|
||||||
|
1. Browser öffnen: `http://localhost`
|
||||||
|
2. Log-Datei über das Upload-Feld hochladen
|
||||||
|
3. Statistiken und Charts werden automatisch geladen (alle 15 Sek. refreshed)
|
||||||
|
4. LLM-Analyse über den Bereich unten starten
|
||||||
|
|
||||||
|
### API (cURL)
|
||||||
|
|
||||||
|
#### Log-Datei hochladen
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8080/api/upload \
|
curl -X POST http://localhost/api/upload \
|
||||||
-H "Content-Type: multipart/form-data" \
|
-H "Content-Type: multipart/form-data" \
|
||||||
-F "file=@/var/log/iptables.log"
|
-F "file=@/var/log/iptables.log"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Statistiken abrufen
|
#### Statistiken abrufen
|
||||||
```bash
|
```bash
|
||||||
curl "http://localhost:8080/api/stats?limit=20"
|
curl "http://localhost/api/stats?limit=20"
|
||||||
```
|
```
|
||||||
|
|
||||||
### LLM-Analyse starten
|
#### LLM-Analyse starten
|
||||||
```bash
|
```bash
|
||||||
curl -X POST "http://localhost:8080/api/analyze?log_type=firewall&limit=100"
|
curl -X POST "http://localhost/api/analyze?log_type=firewall&limit=100"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gesundheitscheck
|
#### Gesundheitscheck
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8080/health
|
curl http://localhost/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## API-Endpunkte
|
## API-Endpunkte
|
||||||
@@ -79,6 +88,7 @@ curl http://localhost:8080/health
|
|||||||
| GET | `/api/stats/ports` | Top Ports |
|
| GET | `/api/stats/ports` | Top Ports |
|
||||||
| POST | `/api/analyze` | LLM-Analyse der Logs |
|
| POST | `/api/analyze` | LLM-Analyse der Logs |
|
||||||
| GET | `/health` | Healthcheck |
|
| GET | `/health` | Healthcheck |
|
||||||
|
| GET | `/docs` | Swagger UI (FastAPI) |
|
||||||
|
|
||||||
## Umgebungsvariablen
|
## Umgebungsvariablen
|
||||||
|
|
||||||
@@ -103,6 +113,7 @@ curl http://localhost:8080/health
|
|||||||
- Das Backend erwartet Ollama auf dem **Host** (nicht im Container). Für Linux ggf. `extra_hosts: ["host.docker.internal:host-gateway"]` nutzen.
|
- Das Backend erwartet Ollama auf dem **Host** (nicht im Container). Für Linux ggf. `extra_hosts: ["host.docker.internal:host-gateway"]` nutzen.
|
||||||
- Große Logdateien werden in Batches von 500 Zeilen verarbeitet.
|
- Große Logdateien werden in Batches von 500 Zeilen verarbeitet.
|
||||||
- Die SQLite-Datenbank wird im Docker-Volumen `logdata` persistiert.
|
- Die SQLite-Datenbank wird im Docker-Volumen `logdata` persistiert.
|
||||||
|
- Das Frontend aktualisiert die Statistiken automatisch alle 15 Sekunden.
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
+4
-6
@@ -14,13 +14,11 @@ services:
|
|||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
nginx:
|
frontend:
|
||||||
image: nginx:alpine
|
build: ./frontend
|
||||||
container_name: log-analyzer-nginx
|
container_name: log-analyzer-frontend
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "80:80"
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY . /usr/share/nginx/html/
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0f1117;
|
||||||
|
--panel: #181b23;
|
||||||
|
--accent: #38bdf8;
|
||||||
|
--accent2: #22d3ee;
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--border: #27303f;
|
||||||
|
--success: #22c55e;
|
||||||
|
--danger: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: linear-gradient(90deg, rgba(56,189,248,0.15), transparent);
|
||||||
|
}
|
||||||
|
header h1 { margin: 0; font-size: 1.8rem; }
|
||||||
|
.subtitle { color: var(--muted); font-size: 0.95rem; }
|
||||||
|
|
||||||
|
main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.upload-card h2 { margin-top: 0; font-size: 1.15rem; }
|
||||||
|
.upload-card input[type=file] { margin-right: 0.75rem; }
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||||
|
color: #0f1117;
|
||||||
|
border: none;
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.msg { margin-top: 0.75rem; padding: 0.5rem 0.75rem; border-radius: 0.5rem; }
|
||||||
|
.msg.ok { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
|
||||||
|
.msg.err { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-label { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.stat-value { font-size: 2rem; font-weight: 800; margin-top: 0.25rem; color: var(--accent); }
|
||||||
|
|
||||||
|
.charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.chart-card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.chart-card.wide { grid-column: 1 / -1; }
|
||||||
|
.chart-card h3 { margin-top: 0; font-size: 1rem; color: var(--muted); }
|
||||||
|
|
||||||
|
canvas { max-height: 260px; }
|
||||||
|
|
||||||
|
.tables h3, .actions h3 { margin: 0 0 0.5rem; }
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
th, td { padding: 0.6rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
|
||||||
|
th { color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
.url { word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: var(--accent2); }
|
||||||
|
|
||||||
|
.action-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||||
|
.tag {
|
||||||
|
background: rgba(56,189,248,0.12);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid rgba(56,189,248,0.25);
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.analyze-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-bottom: 1rem; }
|
||||||
|
.analyze-controls label { display: flex; align-items: center; gap: 0.5rem; color: var(--muted); }
|
||||||
|
.analyze-controls input, .analyze-controls select {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-result {
|
||||||
|
background: #0b0d12;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.analysis-result pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; color: #c7d2e2; }
|
||||||
|
|
||||||
|
footer { text-align: center; padding: 2rem; color: var(--muted); }
|
||||||
|
footer a { color: var(--accent); }
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Log Analyzer Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header>
|
||||||
|
<h1>🔥 Log Analyzer</h1>
|
||||||
|
<span class="subtitle">Firewall & Proxy Log Dashboard</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="upload-card">
|
||||||
|
<h2>📤 Log-Datei hochladen</h2>
|
||||||
|
<input type="file" @change="handleFile" accept=".log,.txt,.csv">
|
||||||
|
<button @click="uploadFile" :disabled="!selectedFile || uploading">
|
||||||
|
{{ uploading ? 'Hochladen...' : 'Hochladen & Parsen' }}
|
||||||
|
</button>
|
||||||
|
<div v-if="uploadMsg" :class="['msg', uploadOk ? 'ok' : 'err']">{{ uploadMsg }}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="stats-grid" v-if="stats.overview.total_entries > 0">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Gesamt Logs</div>
|
||||||
|
<div class="stat-value">{{ stats.overview.total_entries }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Firewall</div>
|
||||||
|
<div class="stat-value">{{ stats.overview.firewall_entries }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Proxy</div>
|
||||||
|
<div class="stat-value">{{ stats.overview.proxy_entries }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Unique Sources</div>
|
||||||
|
<div class="stat-value">{{ stats.unique_counts.unique_sources }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Unique Destinations</div>
|
||||||
|
<div class="stat-value">{{ stats.unique_counts.unique_destinations }}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="charts-grid" v-if="stats.overview.total_entries > 0">
|
||||||
|
<div class="chart-card">
|
||||||
|
<h3>Top Quellen</h3>
|
||||||
|
<canvas id="chartSources"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card">
|
||||||
|
<h3>Top Ziele</h3>
|
||||||
|
<canvas id="chartDestinations"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card">
|
||||||
|
<h3>Top Ports</h3>
|
||||||
|
<canvas id="chartPorts"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card wide">
|
||||||
|
<h3>Zeitverlauf (pro Stunde)</h3>
|
||||||
|
<canvas id="chartTimeline"></canvas>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tables" v-if="stats.top_urls.length > 0">
|
||||||
|
<h3>🔝 Top URLs</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>URL</th><th>Hits</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="u in stats.top_urls.slice(0,20)" :key="u.url">
|
||||||
|
<td class="url">{{ u.url }}</td>
|
||||||
|
<td>{{ u.count }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="actions" v-if="stats.actions.length > 0">
|
||||||
|
<h3>⚡ Actions</h3>
|
||||||
|
<div class="action-tags">
|
||||||
|
<span v-for="a in stats.actions" :key="a.action" class="tag">
|
||||||
|
{{ a.action }}: {{ a.count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="analyze">
|
||||||
|
<h2>🤖 LLM-Analyse</h2>
|
||||||
|
<div class="analyze-controls">
|
||||||
|
<label>Log-Typ:
|
||||||
|
<select v-model="analyzeType">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="firewall">Firewall</option>
|
||||||
|
<option value="proxy">Proxy</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Zeilen:
|
||||||
|
<input type="number" v-model.number="analyzeLimit" min="10" max="500" step="10">
|
||||||
|
</label>
|
||||||
|
<button @click="runAnalysis" :disabled="analyzing || stats.overview.total_entries === 0">
|
||||||
|
{{ analyzing ? 'Analysiere...' : 'Analyse starten' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisResult" class="analysis-result">
|
||||||
|
<pre>{{ analysisResult }}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<small>Log Analyzer Backend — <a href="/docs">API Docs</a></small>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
const api = axios.create({ baseURL: '/api' });
|
||||||
|
|
||||||
|
const chartColors = [
|
||||||
|
'#38bdf8','#22d3ee','#818cf8','#c084fc','#f472b6','#fb7185','#34d399','#a3e635','#fbbf24','#f87171'
|
||||||
|
];
|
||||||
|
|
||||||
|
function destroyChart(chart) { if(chart) { chart.destroy(); } }
|
||||||
|
|
||||||
|
function makeBarConfig(label, labels, data) {
|
||||||
|
return {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
label,
|
||||||
|
data,
|
||||||
|
backgroundColor: chartColors,
|
||||||
|
borderRadius: 4,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: true, grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } },
|
||||||
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLineConfig(labels, data) {
|
||||||
|
return {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Events',
|
||||||
|
data,
|
||||||
|
borderColor: '#38bdf8',
|
||||||
|
backgroundColor: 'rgba(56,189,248,0.15)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: true, grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } },
|
||||||
|
x: { grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedFile: null,
|
||||||
|
uploading: false,
|
||||||
|
uploadMsg: '',
|
||||||
|
uploadOk: false,
|
||||||
|
stats: {
|
||||||
|
overview: { total_entries: 0, firewall_entries: 0, proxy_entries: 0 },
|
||||||
|
top_sources: [],
|
||||||
|
top_destinations: [],
|
||||||
|
top_ports: [],
|
||||||
|
top_urls: [],
|
||||||
|
actions: [],
|
||||||
|
timeline: [],
|
||||||
|
unique_counts: { unique_sources: 0, unique_destinations: 0 }
|
||||||
|
},
|
||||||
|
analyzeType: 'all',
|
||||||
|
analyzeLimit: 100,
|
||||||
|
analyzing: false,
|
||||||
|
analysisResult: '',
|
||||||
|
charts: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchStats();
|
||||||
|
setInterval(() => this.fetchStats(), 15000);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleFile(e) {
|
||||||
|
this.selectedFile = e.target.files[0];
|
||||||
|
},
|
||||||
|
async uploadFile() {
|
||||||
|
if (!this.selectedFile) return;
|
||||||
|
this.uploading = true;
|
||||||
|
this.uploadMsg = '';
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', this.selectedFile);
|
||||||
|
try {
|
||||||
|
const res = await api.post('/upload', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||||
|
this.uploadOk = true;
|
||||||
|
this.uploadMsg = res.data.message;
|
||||||
|
this.selectedFile = null;
|
||||||
|
this.$el.querySelector('input[type=file]').value = '';
|
||||||
|
this.fetchStats();
|
||||||
|
} catch (e) {
|
||||||
|
this.uploadOk = false;
|
||||||
|
this.uploadMsg = e.response?.data?.detail || 'Upload fehlgeschlagen.';
|
||||||
|
} finally {
|
||||||
|
this.uploading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchStats() {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/stats', { params: { limit: 20 } });
|
||||||
|
this.stats = res.data;
|
||||||
|
this.$nextTick(() => this.renderCharts());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Stats fetch error', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderCharts() {
|
||||||
|
const s = this.stats;
|
||||||
|
|
||||||
|
destroyChart(this.charts.sources);
|
||||||
|
if (s.top_sources.length) {
|
||||||
|
const cfg = makeBarConfig('Hits', s.top_sources.map(x=>x.source_ip||'N/A'), s.top_sources.map(x=>x.count));
|
||||||
|
this.charts.sources = new Chart(document.getElementById('chartSources'), cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyChart(this.charts.dests);
|
||||||
|
if (s.top_destinations.length) {
|
||||||
|
const cfg = makeBarConfig('Hits', s.top_destinations.map(x=>x.destination_ip||'N/A'), s.top_destinations.map(x=>x.count));
|
||||||
|
this.charts.dests = new Chart(document.getElementById('chartDestinations'), cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyChart(this.charts.ports);
|
||||||
|
if (s.top_ports.length) {
|
||||||
|
const cfg = makeBarConfig('Hits', s.top_ports.map(x=>x.destination_port||'N/A'), s.top_ports.map(x=>x.count));
|
||||||
|
this.charts.ports = new Chart(document.getElementById('chartPorts'), cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyChart(this.charts.timeline);
|
||||||
|
if (s.timeline.length) {
|
||||||
|
const cfg = makeLineConfig(s.timeline.map(x=>x.time_bucket), s.timeline.map(x=>x.count));
|
||||||
|
this.charts.timeline = new Chart(document.getElementById('chartTimeline'), cfg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async runAnalysis() {
|
||||||
|
this.analyzing = true;
|
||||||
|
this.analysisResult = '';
|
||||||
|
try {
|
||||||
|
const res = await api.post('/analyze', null, {
|
||||||
|
params: { log_type: this.analyzeType, limit: this.analyzeLimit }
|
||||||
|
});
|
||||||
|
this.analysisResult = res.data.analysis;
|
||||||
|
} catch (e) {
|
||||||
|
this.analysisResult = e.response?.data?.detail || 'Analyse fehlgeschlagen.';
|
||||||
|
} finally {
|
||||||
|
this.analyzing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
@@ -3,6 +3,10 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
upstream backend {
|
upstream backend {
|
||||||
server backend:8000;
|
server backend:8000;
|
||||||
}
|
}
|
||||||
@@ -10,13 +14,22 @@ http {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
location / {
|
location /api/ {
|
||||||
proxy_pass http://backend;
|
proxy_pass http://backend/api/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user