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 +
+ +
+
+

📤 Log-Datei hochladen

+ + +
{{ uploadMsg }}
+
+ +
+
+
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

+ + + + + + + + +
URLHits
{{ u.url }}{{ u.count }}
+
+ +
+

⚡ Actions

+
+ + {{ a.action }}: {{ a.count }} + +
+
+ +
+

🤖 LLM-Analyse

+
+ + + +
+
+
{{ analysisResult }}
+
+
+
+ + +
+ + + 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; } } }