diff --git a/README.md b/README.md
index 5069e8d..86921db 100644
--- a/README.md
+++ b/README.md
@@ -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
- **Log-Parsing:** Unterstützt iptables, pfSense, Cisco ASA, Squid, nginx
- **Statistiken:** Top Quellen, Ziele, Ports, URLs, Actions, Timeline
- **LLM-Analyse:** Zusammenfassung und Anomalie-Erkennung via lokalem Ollama-Modell
- **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
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
-│ Client │──────▶│ nginx:80 │──────▶│ FastAPI Backend:8000 │
-└──────────────┘ └──────────────┘ │ - SQLite (Volumen) │
- │ - Log-Parser │
- │ - Ollama-Client │
+│ Browser │──────▶│ Frontend:80 │──────▶│ FastAPI Backend:8000 │
+└──────────────┘ │ (nginx + │ │ - SQLite (Volumen) │
+ │ Vue SPA) │ │ - Log-Parser │
+ └──────────────┘ │ - Ollama-Client │
└───────────────────────┘
│
▼
@@ -29,7 +30,7 @@ CPU-LLM-gestütztes Backend zur Auswertung von Firewall- und Proxy-Logs. Parst D
## Voraussetzungen
- Docker + Docker Compose
- 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
@@ -42,30 +43,38 @@ cd log-analyzer-backend
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
-### 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
-curl -X POST http://localhost:8080/api/upload \
+curl -X POST http://localhost/api/upload \
-H "Content-Type: multipart/form-data" \
-F "file=@/var/log/iptables.log"
```
-### Statistiken abrufen
+#### Statistiken abrufen
```bash
-curl "http://localhost:8080/api/stats?limit=20"
+curl "http://localhost/api/stats?limit=20"
```
-### LLM-Analyse starten
+#### LLM-Analyse starten
```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
-curl http://localhost:8080/health
+curl http://localhost/health
```
## API-Endpunkte
@@ -79,6 +88,7 @@ curl http://localhost:8080/health
| GET | `/api/stats/ports` | Top Ports |
| POST | `/api/analyze` | LLM-Analyse der Logs |
| GET | `/health` | Healthcheck |
+| GET | `/docs` | Swagger UI (FastAPI) |
## 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.
- Große Logdateien werden in Batches von 500 Zeilen verarbeitet.
- Die SQLite-Datenbank wird im Docker-Volumen `logdata` persistiert.
+- Das Frontend aktualisiert die Statistiken automatisch alle 15 Sekunden.
## Lizenz
MIT
diff --git a/docker-compose.yml b/docker-compose.yml
index 0192721..d74a386 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,13 +14,11 @@ services:
- "host.docker.internal:host-gateway"
restart: unless-stopped
- nginx:
- image: nginx:alpine
- container_name: log-analyzer-nginx
+ frontend:
+ build: ./frontend
+ container_name: log-analyzer-frontend
ports:
- - "8080:80"
- volumes:
- - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+ - "80:80"
depends_on:
- backend
restart: unless-stopped
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..4605b31
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,3 @@
+FROM nginx:alpine
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY . /usr/share/nginx/html/
diff --git a/frontend/css/style.css b/frontend/css/style.css
new file mode 100644
index 0000000..94f985c
--- /dev/null
+++ b/frontend/css/style.css
@@ -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); }
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..c4c3727
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+Log Analyzer Dashboard
+
+
+
+
+
+
+
+
+ 🔥 Log Analyzer
+ Firewall & Proxy Log Dashboard
+
+
+
+
+
+
+
+
Gesamt Logs
+
{{ stats.overview.total_entries }}
+
+
+
Firewall
+
{{ stats.overview.firewall_entries }}
+
+
+
Proxy
+
{{ stats.overview.proxy_entries }}
+
+
+
Unique Sources
+
{{ stats.unique_counts.unique_sources }}
+
+
+
Unique Destinations
+
{{ stats.unique_counts.unique_destinations }}
+
+
+
+
+
+
Top Quellen
+
+
+
+
Top Ziele
+
+
+
+
Top Ports
+
+
+
+
Zeitverlauf (pro Stunde)
+
+
+
+
+
+ 🔝 Top URLs
+
+ | URL | Hits |
+
+
+ | {{ u.url }} |
+ {{ u.count }} |
+
+
+
+
+
+
+ ⚡ Actions
+
+
+ {{ a.action }}: {{ a.count }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/js/app.js b/frontend/js/app.js
new file mode 100644
index 0000000..6073ddd
--- /dev/null
+++ b/frontend/js/app.js
@@ -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');
diff --git a/nginx/nginx.conf b/frontend/nginx.conf
similarity index 52%
rename from nginx/nginx.conf
rename to frontend/nginx.conf
index d6fe1b4..b192cdf 100644
--- a/nginx/nginx.conf
+++ b/frontend/nginx.conf
@@ -3,6 +3,10 @@ events {
}
http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+ sendfile on;
+
upstream backend {
server backend:8000;
}
@@ -10,13 +14,22 @@ http {
server {
listen 80;
client_max_body_size 100M;
+ root /usr/share/nginx/html;
+ index index.html;
- location / {
- proxy_pass http://backend;
+ location /api/ {
+ proxy_pass http://backend/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
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;
}
}
}