Nur Department Produktion via ProduktionAssetWizard
-
-
-
Benutzer
-
✓
-
✗
-
✗
-
✗
-
✗
-
Nur eigene Assets via /assets/mine
-
-
-
-
-
△ = Eingeschränkt auf Produktions-Department
-
-
-
-
-
-
-
-
RMM-Roadmap — Nächste Ausbaustufen
-
-
Geplante Erweiterungen für vollständiges Remote Monitoring & Management
-
-
-
Architektur-Übersicht: Geräteklassen & Anbindung
-
-
-
-
-
-
-
🖥️
-
-
Windows Endgeräte
- ✅ Vorhanden
-
-
-
-
Lösung
-
IT Nexus Agent v2.0.0
-
-
-
• C# Agent als Windows Service
-
• Check-in alle 1 Minute (SYSTEM)
-
• Patch Management, Ankündigungen
-
• Auto-Update + Staged Rollout
-
• Intune-Deployment via .exe Installer
-
-
-
-
-
-
-
-
🗄️
-
-
Windows Server
- 🔄 Geplant
-
-
-
-
Lösung
-
IT Nexus Agent (Server-Modus)
-
-
-
• Gleicher Agent wie auf Clients
-
• Erweitert um Server-Metriken
-
• Dienste, Eventlog, RAID, IIS/SQL
-
• Kein externes Tool nötig
-
• Alles in IT Nexus integriert
-
-
-
-
-
-
-
-
🐧
-
-
Linux Server
- 🔄 Geplant
-
-
-
-
Lösung
-
SSH direkt aus IT Nexus
-
-
-
• Kein extra Tool erforderlich
-
• SSH-Polling direkt aus Backend
-
• CPU, RAM, Disk, Dienste
-
• Unterstützt Proxmox LXC + VMs
-
• Key-basierte Auth (kein Passwort)
-
-
-
-
-
-
-
Was kommt als nächstes
-
-
-
-
-
Feature
-
Beschreibung
-
Aufwand
-
Priorität
-
-
-
-
-
Fernzugriff
-
MeshCentral self-hosted (Docker), API-Link aus IT Nexus direkt in Session
-
Mittel
-
🔴 Hoch
-
-
-
Backup-Monitoring
-
Agent meldet Veeam/Windows Backup Status; IT Nexus zeigt letztes Backup + Erfolg/Fehler
-
Klein
-
🔴 Hoch
-
-
-
Server-Monitoring
-
IT Nexus Agent v2.0.0 auf Windows Servern deployen — Server-spezifische Metriken: Dienste-Status, Eventlog-Fehler, RAID, Windows Update
-
Klein
-
🔴 Hoch
-
-
-
Linux Server Monitoring
-
SSH-basiertes Polling (CPU, RAM, Disk, Dienste) direkt aus Backend
-
Mittel
-
🟡 Mittel
-
-
-
Health-Score
-
Pro Gerät/Kunde: Gewichteter Score aus Patch-Status, Backup, Online, Defender
-
Groß
-
⚪ Niedrig
-
-
-
Multi-Tenant
-
Kundenmandanten trennen — Grundstruktur für MSP-Betrieb
-
Groß
-
⚪ Niedrig
-
-
-
-
-
-
-
-
💡
-
-
Server-Empfehlung: IT Nexus Agent auf Windows Servern
-
- Bestehenden IT Nexus Agent (v2.0.0) auch auf Windows Servern deployen. In zukünftigen Versionen Server-spezifische Metriken ergänzen — kein externes Tool, alles in IT Nexus integriert.
-
-
-
-
-
-
-
-
diff --git a/SYSTEM_AUDIT_2026-05-07.md b/SYSTEM_AUDIT_2026-05-07.md
deleted file mode 100644
index 9d01ed7..0000000
--- a/SYSTEM_AUDIT_2026-05-07.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# IT Nexus System Audit — 07.05.2026
-**Erstellt von:** Claude (Read-Only, keine Änderungen)
-**Status:** Offen — noch nicht bearbeitet
-
----
-
-## Executive Summary
-
-Das System läuft stabil. Frontend und Backend sind erreichbar, die letzten 10+ Tage alle grün im Health-Check. Es gibt aber konkrete Probleme in den Bereichen Sicherheit, Konsistenz und Wartbarkeit.
-
----
-
-## 🟢 Was gut ist
-
-| Bereich | Status |
-|---|---|
-| System erreichbar (HTTPS) | ✅ |
-| Health: letzte 10 Tage | ✅ alle grün |
-| Agent-Auth (X-Agent-Key) | ✅ korrekt |
-| JWT-Auth auf geschützten Routes | ✅ korrekt |
-| Rate-Limiting auf Login | ✅ vorhanden |
-| DB-Migrations (try/catch) | ✅ idempotent |
-| asyncHandler auf Controllern | ✅ konsistent |
-
----
-
-## 🔴 Kritische Probleme (sofort)
-
-### 1. Agent-Version Default falsch
-- `AGENT_VERSION` Fallback im Code noch auf `'1.2.4'` — zieht sich durch 3 Stellen im Controller
-- **Fix:** In docker-compose.yml `AGENT_VERSION=2.0.0` — bereits gesetzt ✅. Aber Fallback im Code noch `'1.2.4'` und `'1.2.3'` — sollte auf `'2.0.0'` geändert werden
-- **Dateien:** `backend/src/controllers/monitoringAgent.controller.js` Zeilen 25, 113, 136
-
-### 2. `/api/monitoring/agent-setup` ohne vollständige Auth
-- Endpoint prüft Key im Controller, aber Route liegt vor Middleware — unkonventionell
-- Kein HTTPS-erzwungener Download
-
-### 3. SSE-Endpoints mit Token im Query-Parameter
-- `/api/tickets/:id/events?token=xxx` — Token landet in Server-Logs und Browser-History
-- `/api/network-monitor/sse?token=xxx` — gleiches Problem
-- **Dateien:** `backend/src/routes/ticket.routes.js`, `backend/src/routes/networkMonitor.routes.js`
-
----
-
-## 🟡 Hohe Priorität (diese Woche)
-
-### 4. Kein Rate-Limiting außer Login
-- `POST /api/tickets/public` — jeder kann Tickets erstellen, keine Limits
-- `POST /api/tickets/:id/comments` — Spam möglich
-- **Datei:** `backend/src/server.js` Zeilen 64-74
-
-### 5. Keine Pagination auf großen Endpoints
-- `GET /api/users` → alle User auf einmal
-- `GET /api/assets` → alle Assets auf einmal
-- `GET /api/tickets` → alle Tickets
-- Bei Wachstum wird das ein Performance-Problem
-
-### 6. E-Mail-Fehler werden ignoriert
-- `.catch(err => console.error())` ohne Retry
-- Bestätigungs-Mails, Onboarding-Mails könnten lautlos verloren gehen
-- **Dateien:** `backend/src/controllers/ticket.controller.js` Zeilen 131, 137, 178
-
-### 7. Kein Graceful Shutdown der DB
-- SQLite wird beim Neustart nicht sauber geschlossen → mögliche Locks
-- **Datei:** `backend/src/server.js` (fehlt: `process.on('SIGTERM')` Handler)
-
----
-
-## 🟠 Mittlere Priorität (diesen Monat)
-
-### 8. Kein strukturiertes Logging
-- Überall `console.log/error` — kein Log-Level, kein JSON-Format
-- Schwer zu debuggen in Produktion
-- **Empfehlung:** Winston oder Pino einführen
-
-### 9. Planner-Sync läuft alle 10 Sekunden
-- Sehr aggressiv — sollte 5 Minuten sein
-- **Datei:** `backend/src/server.js` Zeile 274-290
-
-### 10. CORS in Dev auf `*`
-- Alle Ursprünge erlaubt — sollte auch in Dev eingeschränkt sein
-- **Datei:** `backend/src/server.js` Zeilen 46-53
-
-### 11. Onboarding-PII unverschlüsselt
-- `emp_phone`, `emp_address`, `emp_private_email` in Klartext in DB
-- DSGVO-relevant bei DB-Leak
-- **Tabelle:** `onboarding_protocols`
-
-### 12. Schema-Inkonsistenz Asset-Typen
-- DB-Schema kennt 4 Typen, seed.js seeded 9 Typen — CHECK-Constraint wird umgangen
-- **Datei:** `backend/src/db/seed.js`
-
-### 13. Audit-Logging unvollständig
-- Password-Änderungen nicht gelogged
-- Token-Generierungen nicht gelogged
-- **Empfehlung:** Alle Auth-Events in `audit_log` schreiben
-
----
-
-## 📊 Vollständige Route-Übersicht
-
-### Auth Routes (`/api/auth`)
-| Methode | Pfad | Auth |
-|---|---|---|
-| POST | /login | Public (Rate-Limited) |
-| GET | /me | JWT |
-| POST | /change-password | JWT |
-| PUT | /notifications | JWT |
-| PUT | /staff-notifications | JWT |
-| GET | /microsoft | Public |
-| GET | /microsoft/callback | Public |
-| GET | /microsoft/addin | Public |
-| GET | /microsoft/addin-callback | Public |
-| GET | /microsoft/addin-result/:sessionId | Public |
-
-### Monitoring Routes (`/api/monitoring`)
-| Methode | Pfad | Auth |
-|---|---|---|
-| POST | /checkin | X-Agent-Key |
-| POST | /announcements-poll | X-Agent-Key |
-| GET | /agent-script | X-Agent-Key |
-| GET | /agent-setup | X-Agent-Key (Controller) |
-| GET | /statistics | JWT + Admin |
-| GET | / | JWT + Admin |
-| GET | /:id | JWT + Admin |
-| DELETE | /:id | JWT + Admin |
-
-### Patch Routes (`/api/patch`)
-| Methode | Pfad | Auth |
-|---|---|---|
-| POST | /commands/result | X-Agent-Key |
-| GET | /overview | JWT |
-| GET | /groups | JWT + Admin |
-| POST | /groups | JWT + Admin |
-| PUT | /groups/:id | JWT + Admin |
-| DELETE | /groups/:id | JWT + Admin |
-| POST | /commands/trigger | JWT + Admin |
-| POST | /commands/trigger-group | JWT + Admin |
-| GET | /commands | JWT + Admin |
-
----
-
-## 📋 Datenbank-Tabellen (38 gesamt)
-
-| Tabelle | Zweck |
-|---|---|
-| roles | Benutzerrollen |
-| users | Benutzerkonten |
-| fido_keys | FIDO2-Schlüssel |
-| assets | IT-Assets |
-| asset_assignments | Asset-Zuweisungen |
-| asset_inspections | Inspektionen |
-| asset_movements | Bewegungshistorie |
-| asset_types | Asset-Kategorien |
-| onboarding_protocols | Onboarding |
-| offboarding_protocols | Offboarding |
-| tickets | Support-Tickets |
-| ticket_comments | Kommentare |
-| ticket_links | Ticket-Verknüpfungen |
-| ticket_assignees | Mehrfach-Zuweisungen |
-| ticket_routing | Auto-Routing |
-| monitoring_agents | Windows Agenten |
-| network_devices | Netzwerk-Monitoring |
-| device_checks | Prüf-Ergebnisse |
-| health_history | System-Health |
-| patch_groups | Patch-Gruppen |
-| patch_policies | Patch-Richtlinien |
-| patch_agent_groups | Agent-Gruppen |
-| patch_commands | Patch-Befehle |
-| licenses | Softwarelizenzen |
-| warehouse_locations | Lagerplätze |
-| stock_thresholds | Mindestbestände |
-| purchase_orders | Bestellungen |
-| knowledge_base | Wissensdatenbank |
-| ai_knowledge | KI-Wissensdatenbank |
-| portal_guides | Portal-Guides |
-| announcements | Ankündigungen |
-| announcement_acks | Bestätigungen |
-| external_alerts | Externe Warnungen |
-| teams_channels | Teams-Kanäle |
-| audit_log | Audit-Protokoll |
-| unifi_config | Unifi-Konfiguration |
-| unifi_devices | Unifi-Geräte |
-| it_topics | IT-Übersicht |
-
----
-
-## 📋 Priorisierte To-Do-Liste
-
-| Prio | Aufwand | Was | Datei |
-|---|---|---|---|
-| 1 | 5 Min | Code-Fallback `'1.2.4'` → `'2.0.0'` | monitoringAgent.controller.js |
-| 2 | 30 Min | Rate-Limiting auf public Ticket-Erstellung | server.js |
-| 3 | 30 Min | Pagination auf /api/users, /api/assets, /api/tickets | controllers |
-| 4 | 1h | Graceful Shutdown für SQLite | server.js |
-| 5 | 2h | E-Mail Retry-Logic | ticket.controller.js |
-| 6 | 2h | SSE Token aus Query absichern | routes |
-| 7 | 1 Tag | Strukturiertes Logging einführen | server.js + alle |
-| 8 | 1 Tag | PII-Felder verschlüsseln (DSGVO) | onboarding_protocols |
-
----
-
-## Live-System Stand (07.05.2026)
-
-| | |
-|---|---|
-| Health letzten 10 Tage | ✅ alle OK |
-| Frontend erreichbar | ✅ |
-| API erreichbar | ✅ |
-| Agent v2.0.0 | ✅ IT-NB-02, TBO-NB-02, FAM102223 |
-| Geräte mit altem Agent | ⚠️ ~41 Geräte noch v1.x |
-| CT 110 (AI) | ✅ nach fsck repariert |
-| CT 102, 103, 111 | ✅ alle healthy |
-
----
-*Nächster Schritt: Priorisierung mit Simon besprechen, dann schrittweise abarbeiten.*
diff --git a/Tickets-Demo.html b/Tickets-Demo.html
deleted file mode 100644
index 5ab507a..0000000
--- a/Tickets-Demo.html
+++ /dev/null
@@ -1,1074 +0,0 @@
-
-
-
-
-
-IT Nexus — Tickets
-
-
-
-
-
-
-
IT Nexus
-
›
-
Tickets
-
-
SG
-
-
-
-
-
-
-
-
-
-
- Tickets
- 851
-
-
-
-
-
- Alle
- Offen
- In Bearbeitung
- Gelöst
- Geschlossen
-
-
-
- Alle
- Kritisch
- Hoch
- Mittel
- Niedrig
-
-
-
-
-
-
-
Gesamt
-
851
-
-
-
Offen
-
1
-
-
-
In Bearbeitung
-
0
-
-
-
Kritisch
-
0
-
-
-
-
-
-
-
-
-
-
-
-
-
🎫
-
Kein Ticket ausgewählt
-
-
-
-
-
-
-
-
🎫
-
-
Zugangsdaten - Drucker Zugänge und Passwort Pflege
-
TK-2026-0001 · Allgemein · Erstellt vor 78 Tagen
-
- Hoch
- Offen
- Web
-
-
-
-
-
-
-
-
● Offen
-
● In Bearbeitung
-
● Gelöst
-
● Geschlossen
-
-
-
-
-
-
-
-
-
-
Erstellt
-
15.03.2026
-
von Simon Grüßing
-
-
-
Priorität
-
Hoch
-
-
-
Zugewiesen an
-
? Nicht zugewiesen
-
-
-
SLA
-
78 Tage offen
-
⚠ Überfällig
-
-
-
-
-
-
Beschreibung
-
Kommentare 2
-
Verlauf
-
Verknüpfte Assets
-
-
-
-
-
-
Beschreibung
-
- Zugangsdaten für Drucker müssen gepflegt werden. Aktuelle Passwörter sind nicht dokumentiert.
- Bitte alle Drucker-Zugänge sammeln und im Passwort-Manager hinterlegen.
-
-
-
-
-
-
Details
-
- Kategorie
- Allgemein
-
-
- Quelle
- Web
-
-
- Erstellt am
- 15.03.2026
-
-
- Zuletzt aktualisiert
- 12.04.2026
-
-
- Ticket-Nummer
- TK-2026-0001
-
-
-
-
-
Zuweisung & SLA
-
- Zugewiesen an
- —
-
-
- Ersteller
- Simon Grüßing
-
-
- SLA-Status
- ⚠ 78 Tage überfällig
-
-
-
SLA-Auslastung
-
-
-
-
100% — Überschritten
-
-
-
-
-
-
-
-
-
Kommentare (2)
-
-
-
SG
-
-
Simon Grüßing · vor 45 Tagen
-
Habe die Drucker in der Produktion bereits überprüft. Passwörter werden noch gesammelt.
-
-
-
-
-
SG
-
-
Simon Grüßing · vor 20 Tagen
-
Drucker im Büro sind dokumentiert. Produktion steht noch aus.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Aktivitätsverlauf
-
-
-
Ticket erstellt
-
15.03.2026 · von Simon Grüßing
-
-
-
Status geändert: Offen → In Bearbeitung
-
01.04.2026 · von Simon Grüßing
-
-
-
Status geändert: In Bearbeitung → Offen(zurückgesetzt)
-
15.04.2026 · von Simon Grüßing
-
-
-
-
-
-
-
-
-
Verknüpfte Assets
-
-
🖥️
-
Keine Assets verknüpft
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ZELO_5.3_Installationsanleitung.md b/ZELO_5.3_Installationsanleitung.md
deleted file mode 100644
index 62a0816..0000000
--- a/ZELO_5.3_Installationsanleitung.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# ZELO 5.3 — Installationsanleitung
-**Winkel Ruf-Leitsysteme | Version 5.3.0.28**
-
----
-
-## Voraussetzungen
-- Windows 10/11 (x64)
-- SQL Server 2019 Express muss bereits installiert sein (siehe ZELO 6.0 Anleitung, Phase 1)
-- Setup-Datei: `SetupZeloConfig.exe`
-- Pfad: `C:\Winkel Soft\02 Zelo (5.3_5.4_6.0)\01 ZELO5.3\Winkel_Soft\01 ZELO Setup\zeloSetup 5.3.0.28\`
-- Installer: `SetupZeloConfig.exe` (1.553 KB)
-- Administratorrechte auf dem Zielrechner
-
----
-
-## Installation
-
-### Schritt 1 — Startmenü
-- `SetupZeloConfig.exe` starten
-- Zwei Optionen erscheinen:
- - **"zelo Config"** → auswählen
- - "Microsoft® SQL Server® 2019 Express" → **nicht auswählen** (bereits installiert)
-
-### Schritt 2 — AGB akzeptieren
-- Checkbox **"Ich habe die AGB der Winkel GmbH gelesen und akzeptiere diese"** aktivieren ✅
-- Klick: **Weiter**
-
-### Schritt 3 — Eingaben zur Installation
-- Funktion: **Server** auswählen (USB/CAN-Adapter ist an diesem PC angeschlossen)
-- SQL-Servername: **`localhost\ZELO`**
-- Benutzer: **`sa`** (manuell eingeben)
-- Passwort: **`Cered@ZeloFL24`** (manuell eingeben)
-- Zielverzeichnis: `C:\zeloConfig` (Standard lassen)
-- Klick: **Verbindungstest** — grünes Symbol muss erscheinen ✅
-- Klick: **Weiter**
-
-> ⚠️ Alle Felder müssen manuell befüllt werden — werden nicht automatisch übernommen.
-
-### Schritt 4 — Komponenten auswählen
-- Alle drei Komponenten aktivieren:
- - ✅ **zelo Config, Benutzeroberfläche**
- - ✅ **zelo Server, Windows Dienst zur Anbindung des zelo Systems über den CAN-Adapter**
- - ✅ **zelo DB, Beispieldatenbank**
-- Klick: **Weiter**
-
-### Schritt 5 — Zusammenfassung prüfen
-- Angaben kontrollieren:
- - Art der Lizenz: `zelo Server`
- - SQLServer: `localhost\ZELO`
- - Benutzer: `sa`
- - Zielverzeichnis: `C:\zeloConfig`
-- Klick: **"zelo Config jetzt installieren"**
-
-### Schritt 6 — Installation läuft
-- Fortschrittsbalken läuft durch
-- Warten bis "Installation ist abgeschlossen..." erscheint
-- Klick: **Weiter**
-
-### Schritt 7 — Abschließen
-- Haken so lassen:
- - ✅ Desktopverknüpfung erstellen
- - ✅ Eintrag im Startmenü hinzufügen
- - ☐ Bei Computerstart automatisch starten (leer lassen)
-- Klick: **Fertigstellen**
-
----
-
-## Zugangsdaten
-
-| Parameter | Wert |
-|---|---|
-| SQL-Instanz | `localhost\ZELO` |
-| SA-Benutzer | `sa` |
-| SA-Passwort | `Cered@ZeloFL24` |
-| Installationsverzeichnis | `C:\zeloConfig` |
-
----
-
-*Erstellt: Mai 2026 | Cereda Systems GmbH*
diff --git a/ZELO_Installationsanleitung.md b/ZELO_Installationsanleitung.md
deleted file mode 100644
index f5a5972..0000000
--- a/ZELO_Installationsanleitung.md
+++ /dev/null
@@ -1,163 +0,0 @@
-# ZELO 6.0 — Installationsanleitung
-**Winkel Ruf-Leitsysteme | Version 6.0.0.62**
-
----
-
-## Voraussetzungen
-- Windows 10/11 (x64)
-- Setup-Datei: `Setup_ZELO.exe` (V6.0.0.62)
-- Administratorrechte auf dem Zielrechner
-
----
-
-## Phase 0: Vorbereitung — PowerShell (vor dem Setup!)
-
-> ⚠️ Dieser Schritt muss **vor** dem Start des Setups ausgeführt werden, da Windows Dateien von Netzlaufwerken/USB blockiert. Das Setup öffnet sich danach automatisch.
-
-**PowerShell als Administrator** öffnen und folgenden Befehl ausführen:
-
-```powershell
-# ZELO Setup Installer
-# Dateien entsperren und Setup starten
-
-$setupPath = 'C:\Winkel Soft\02 Zelo (5.3_5.4_6.0)\03 ZELO6.0 (inkl 5.4)\Winkel_Soft\01 ZELO Setup\ZeloSetup 6.0.0.62'
-
-Write-Host "Entsperre alle Dateien..." -ForegroundColor Cyan
-Get-ChildItem -Path $setupPath -Recurse | Unblock-File
-
-Write-Host "Starte Setup..." -ForegroundColor Green
-Start-Process -FilePath "$setupPath\Setup_ZELO.exe" -Verb RunAs -Wait
-
-Write-Host "Fertig!" -ForegroundColor Green
-```
-
-Das Script:
-1. Entsperrt alle Dateien im Setup-Ordner (`Unblock-File`)
-2. Startet `Setup_ZELO.exe` automatisch mit Administratorrechten
-3. Wartet bis das Setup abgeschlossen ist
-
----
-
-## Phase 1: SQL Server 2019 installieren
-
-### Schritt 1 — System Check
-- `Setup_ZELO.exe` als Administrator starten
-- Installationstyp: **SQL Server** auswählen
-- Modus: **Einzelplatzinstallation**
-- Klick: **Weiter**
-
-### Schritt 2 — Parameter
-- Installationsverzeichnis: `C:\Program Files\ZELO Config` (Standard)
-- SQL-Instanzname: `ZELO`
-- Anmeldename: `sa`
-- Passwort: `Olez_2013` *(wird später in Schritt 10 auf `Cered@ZeloFL24` geändert)*
-- Klick: **Installieren**
-
-> SQL Server 2019 Express wird jetzt im Hintergrund heruntergeladen und vorbereitet.
-
-### Schritt 3 — SQL Server 2019 Installationsassistent
-Der SQL Server 2019 Setup-Assistent öffnet sich automatisch.
-
-### Schritt 4 — SQL Lizenzbedingungen
-- Checkbox **"Ich akzeptiere die Lizenzbedingungen"** aktivieren
-- Klick: **Weiter**
-
-### Schritt 5 — Setupdateien installieren
-- Warten bis der Fortschrittsbalken abgeschlossen ist und "Weiter" aktiv wird
-- Klick: **Weiter**
-
-### Schritt 6 — Installationsregeln
-- Firewall-Warnung ist **normal** und kann ignoriert werden
-- Klick: **Weiter**
-
-### Schritt 7 — Funktionsauswahl
-- Standard-Auswahl beibehalten: **Datenbank-Engine-Dienste** + **SQL Server-Replikation**
-- Klick: **Weiter**
-
-### Schritt 8 — Instanzkonfiguration
-- Named Instance: **ZELO** (vorausgefüllt, nicht ändern)
-- Instanz-ID: **ZELO** (vorausgefüllt, nicht ändern)
-- Klick: **Weiter**
-
-### Schritt 9 — Serverkonfiguration
-| Dienst | Konto | Starttyp |
-|---|---|---|
-| SQL Server-Datenbank-Engine | `NT AUTHORITY\SYSTEM` | Automatisch |
-| SQL Server-Browser | `NT AUTHORITY\LOCAL SERVICE` | Automatisch |
-
-- Klick: **Weiter**
-
-### Schritt 10 — Datenbank-Engine-Konfiguration
-- Tab: **Serverkonfiguration**
-- Authentifizierungsmodus: **Gemischter Modus** (SQL Server + Windows)
-- SA-Kennwort: **`Cered@ZeloFL24`** *(beide Felder — Standardpasswort funktioniert nicht!)*
-- SQL Server-Administratoren:
- - **"Aktuellen Benutzer hinzufügen"** klicken
- - ⚠️ Prüfen ob der **richtige User** eingetragen ist (z.B. `WINKEL\Bill`)
- - Falls ein Admin-Account steht: **"Entfernen"** → **"Hinzufügen..."** → korrekten User manuell suchen
-- Klick: **Weiter**
-
-### Schritt 11 — Installationsstatus
-- Warten bis die Installation abgeschlossen ist (Fortschrittsbalken)
-- "Weiter" wird aktiv sobald fertig
-
-### Schritt 12 — Abgeschlossen
-- Alle Komponenten zeigen Status **"Erfolgreich"** ✅
-- Details: "Installation erfolgreich"
-- Klick: **Schließen**
-
----
-
-## Phase 2: ZELO Software installieren
-
-### Schritt 13 — ZELO Setup neu starten & Komponenten auswählen
-- `Setup_ZELO.exe` erneut starten
-- Im System Check erscheinen nun alle Komponenten
-- Die oberen sind bereits installiert (.NET, Ixxat, SQL Server) — **nicht anfassen**
-- Die **drei unteren** aktivieren (Haken setzen):
- - ✅ **ZELO Server 6.x** (6.0.0.62)
- - ✅ **ZELO Client** (6.0.0.62)
- - ✅ **ZELO Server 5.4** (5.4.1.4)
-- Klick: **Weiter**
-
-### Schritt 14 — Parameter (SQL-Verbindung)
-- Installationsverzeichnis: `C:\Program Files\ZELO Config` (Standard lassen)
-- SQL-Instanz wird automatisch erkannt: `COMPUTERNAME\ZELO`
-- Anmeldename: **`sa`**
-- Passwort: **`Cered@ZeloFL24`** eintragen
-- Klick: **"Verbindungstest"** — muss erfolgreich sein ✅
-- Klick: **Installieren**
-
-### Schritt 15 — Installations-Status (Abgeschlossen)
-- Beide Haken können so bleiben:
- - ✅ Verknüpfung auf dem Desktop erstellen
- - ✅ Protokolldatei der Installation speichern (Empfohlen)
-- Klick: **Fertigstellen**
-
----
-
-## Phase 3: Testen
-
-### Schritt 16 — Desktop-Verknüpfung
-- Nach der Installation erscheint auf dem Desktop das Icon **"ZELO Config"**
-
-### Schritt 17 — ZELO Config starten
-- Doppelklick auf das Desktop-Icon "ZELO Config"
-- Ladebildschirm erscheint — Version **6.0.0.62** wird angezeigt
-- Warten bis vollständig geladen
-- ✅ Installation erfolgreich wenn die Anwendung startet
-
----
-
-## Zusammenfassung wichtiger Zugangsdaten
-
-| Parameter | Wert |
-|---|---|
-| SQL-Instanz | `COMPUTERNAME\ZELO` |
-| SA-Benutzer | `sa` |
-| SA-Passwort | `Cered@ZeloFL24` |
-| Installationsverzeichnis | `C:\Program Files\ZELO Config` |
-
----
-
-*Erstellt: Mai 2026 | Cereda Systems GmbH*
diff --git a/_ul b/_ul
deleted file mode 100644
index 5b943d5..0000000
--- a/_ul
+++ /dev/null
@@ -1 +0,0 @@
-mkdir: cannot create directory ‘C:\\gradle-home\\it_nexus_build’: File exists
diff --git a/agent/IT-Nexus-Agent-Setup.hta b/agent/IT-Nexus-Agent-Setup.hta
deleted file mode 100644
index 3633d42..0000000
--- a/agent/IT-Nexus-Agent-Setup.hta
+++ /dev/null
@@ -1,524 +0,0 @@
-
-
-
-
-IT Nexus Agent Setup
-
-
-
-
-
-
-
-
-
IT Nexus Agent
-
Setup 1.0.0
-
-
1
Willkommen
-
2
Konfiguration
-
3
Zusammenfassung
-
4
Installation
-
5
Abgeschlossen
-
-
-
-
-
-
-
-
Willkommen
-
Der IT Nexus Agent wird als Windows-Dienst installiert und sendet alle 5 Minuten Systemdaten an das IT Nexus Dashboard.
-
-
CPU, RAM & Festplatten-Auslastung
-
Installierte Software
-
Windows Update Status
-
Angemeldeter Benutzer & Uptime
-
Netzwerk & Systeminformationen
-
-
-
-
-
-
Konfiguration
-
Serververbindung konfigurieren.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Bereit zur Installation
-
Bitte alles pruefen, dann auf Installieren klicken.
-
-
Verzeichnis
-
Server
-
Scheduled TaskAlle 5 Minuten (SYSTEM)
-
Agent Version1.0.0
-
-
Hinweis: Es erscheint eine UAC-Abfrage fuer Administrator-Rechte.
-
-
-
-
-
Installation
-
Vorbereitung...
-
-
-
-
-
-
-
-
✓
-
Installation abgeschlossen
-
Der IT Nexus Agent ist installiert und aktiv. Er sendet alle 5 Minuten Daten an das Dashboard.
-
Sichtbar unter: IT Nexus → Administration → Agent Monitoring
- Alle Rollen
- 👑 Super Admin
- 🛡️ Admin
- 🎧 Support
- 🔧 Bearbeiter
- 👤 Benutzer
-
-
- ● Aktiv
- ● Inaktiv
-
-
-
-
-
-
-
-
Benutzername
-
E-Mail
-
Name
-
Rolle
-
Status
-
Letzter Login
-
Aktionen
-
-
-
-
-
-
-
-
-
-
-
Benutzerverwaltung
-
-
-
-
-
-
-
- Option B: Avatar-Initialen in der ersten Spalte + Aktionen als 3-Punkte-Menü (spart viel Platz, besonders bei langen Buttons). Rolle inline per Dropdown änderbar.
-
-
-
-
-
-
-
-
-
-
-
Benutzer
-
E-Mail
-
Rolle
-
Status
-
Letzter Login
-
···
-
-
-
-
-
-
-
-
-
-
-
Benutzerverwaltung
-
-
-
-
-
-
-
- Option C: Klick auf eine Zeile öffnet ein Detailpanel rechts. Tabelle bleibt links sichtbar. Kein Modal-Overlay. → Klick auf eine Zeile zum Testen!
-
-
-
-
-
-
-
-
-
-
-
-
Benutzer
-
E-Mail
-
Rolle
-
Status
-
Letzter Login
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Benutzerverwaltung Empfehlung
-
-
-
-
-
-
-
-
- Kombination aus A + B + C: Stats-Kacheln · Filter-Chips · Avatar-Initialen · 3-Punkte-Menü · Slideover-Panel bei Klick auf eine Zeile. → Klick auf eine Zeile öffnet das Detail-Panel!
-
-
-
-
-
-
👥
-
14
Benutzer gesamt
-
-
-
✅
-
11
Aktiv
-
-
-
🚫
-
3
Deaktiviert
-
-
-
⏱️
-
4
Noch nie eingeloggt
-
-
-
-
-
-
-
-
- Alle Rollen
- 👑 Super Admin
- 🛡️ Admin
- 🎧 Support
- 👤 Benutzer
-
-
- ● Aktiv
- ● Inaktiv
-
-
-
-
-
-
-
-
Benutzer
-
E-Mail
-
Rolle
-
Status
-
Letzter Login
-
···
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/devteam.js b/devteam.js
deleted file mode 100644
index 4eec04c..0000000
--- a/devteam.js
+++ /dev/null
@@ -1,997 +0,0 @@
-// IT Nexus Dev Team — Lokaler KI-Entwickler-Agent
-// Start: node devteam.js
-// Dann: http://localhost:4242
-
-const http = require('http');
-const https = require('https');
-
-const API_KEY = process.env.ANTHROPIC_API_KEY || '';
-const PORT = 4242;
-
-// ─── IT Nexus Kontext für alle Agenten ────────────────────────────────────────
-const IT_NEXUS_CONTEXT = `
-# IT Nexus – Vollständiger Stack-Kontext
-
-## Tech Stack
-- Backend: Node.js 18 + Express 4 + SQLite (better-sqlite3, SYNCHRON - kein await!)
-- Frontend: React 18 (Create React App) + Custom CSS (Glassmorphism)
-- Auth: JWT (jsonwebtoken) + bcryptjs
-- KI: @anthropic-ai/sdk (Claude claude-sonnet-4-6)
-- Deployment: Docker Compose + nginx auf LXC CT 102 (192.168.0.194)
-
-## Dateistruktur Backend
-\`\`\`
-backend/src/
- server.js – Express Setup, alle app.use() Registrierungen
- db/seed.js – Alle DB-Migrations (jede in try/catch, idempotent!)
- config/database.js – SQLite Verbindung via better-sqlite3
- middleware/
- auth.js – authenticateToken (JWT prüfen)
- roleCheck.js – requireAdmin, requireStaff Middleware
- errorHandler.js – asyncHandler(fn) Wrapper für alle Controller
- controllers/ – Business Logic, eine Datei pro Feature
- models/ – SQLite Queries, eine Datei pro Entity
- routes/ – Express Router, eine Datei pro Feature
- services/ – Externe APIs (Graph API, Proxmox, Anthropic)
-\`\`\`
-
-## Dateistruktur Frontend
-\`\`\`
-frontend/src/
- pages/ – Alle Seiten (NamingConvention: XxxPage.jsx)
- components/common/
- AppLayout.jsx – Wrapper mit Sidebar + Topbar
- Sidebar.jsx – Navigation
- Topbar.jsx – Header
- context/AuthContext.jsx – Auth State, user Objekt
- services/api.js – Axios Instance mit Interceptors (IMMER verwenden!)
-\`\`\`
-
-## Kritische Konventionen Backend
-1. DB-Migration: Jedes ALTER TABLE / CREATE TABLE einzeln in try/catch in seed.js
-2. Controller immer mit asyncHandler wrappen: const foo = asyncHandler(async (req, res) => {...})
-3. Statische Routen VOR /:id definieren (z.B. /statistics vor /:id)
-4. better-sqlite3 ist SYNCHRON: db.prepare('...').get() – KEIN await!
-5. Exports am Ende: module.exports = { foo, bar }
-
-## Kritische Konventionen Frontend
-1. IMMER import api from '../../services/api' – NIEMALS axios direkt importieren
-2. IMMER user.role_name – NIEMALS user.role (falsches Feld!)
-3. CSS Variablen verwenden:
- - Text: var(--text-primary), var(--text-secondary), var(--text-muted)
- - Hintergrund: var(--bg-card), var(--bg-secondary), var(--border-color)
- - Glassmorphism: backdrop-filter: blur(20px), rgba() Hintergründe
-4. Rollen: super_admin, admin, support, bearbeiter, benutzer, hr_personal, buchhaltung
-5. Neue Seite in AppLayout einbinden (Route in App.jsx)
-
-## Deploy-Workflow
-\`\`\`
-scp datei.js root@192.168.0.194:/opt/it-nexus/backend/src/...
-docker cp /opt/it-nexus/backend/src/.../datei.js fido-backend:/app/src/.../
-docker restart fido-backend
-# Frontend rebuild nur nötig wenn neue Seite/Komponente:
-docker compose build frontend && docker compose up -d frontend
-\`\`\`
-
-## Beispiel: Neue Feature-Struktur
-Backend: controller + model + route + seed.js Migration + server.js Route registrieren
-Frontend: XxxPage.jsx + xxxService.js + Route in App.jsx + Link in Sidebar.jsx
-`;
-
-// ─── Agenten Definitionen ──────────────────────────────────────────────────────
-const AGENTS = {
- pm: {
- name: 'Project Manager',
- emoji: '📋',
- color: '#6366f1',
- system: `Du bist der Project Manager des IT Nexus Entwicklerteams. Du koordinierst alle anderen Agenten.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-1. Analysiere die Anfrage des Benutzers
-2. Erstelle einen klaren Implementierungsplan
-3. Bestimme welche Spezialisten benötigt werden
-4. Fasse am Ende alles zusammen
-
-Antworte auf Deutsch. Sei konkret und strukturiert. Nutze Markdown.
-Erkläre kurz was getan werden muss, dann liste die benötigten Schritte auf.`
- },
-
- architect: {
- name: 'Architect',
- emoji: '🏗️',
- color: '#f59e0b',
- system: `Du bist der System-Architekt des IT Nexus Entwicklerteams.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-- Entscheide über DB-Schema (SQLite Spalten, Typen, Constraints)
-- Entscheide über API-Struktur (Endpoints, HTTP-Methoden, Auth-Level)
-- Entscheide über Datenfluss zwischen Frontend und Backend
-- Identifiziere Abhängigkeiten zu bestehenden Features
-
-Antworte auf Deutsch. Gib konkrete technische Entscheidungen.
-Format: DB-Schema → API-Endpoints → Abhängigkeiten → Besonderheiten`
- },
-
- backend: {
- name: 'Backend Dev',
- emoji: '⚙️',
- color: '#10b981',
- system: `Du bist der Backend-Entwickler des IT Nexus Entwicklerteams. Du schreibst den kompletten Backend-Code.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-- Schreibe vollständige, lauffähige Node.js Dateien
-- Folge exakt den Konventionen (asyncHandler, better-sqlite3 synchron, etc.)
-- Jede Datei vollständig mit korrekten Imports und Exports
-- DB-Migrations in seed.js Format
-
-Format pro Datei:
-### Datei: \`backend/src/pfad/dateiname.js\`
-\`\`\`javascript
-// kompletter Code
-\`\`\`
-
-Antworte auf Deutsch für Erklärungen, Code auf Englisch.`
- },
-
- frontend: {
- name: 'Frontend Dev',
- emoji: '🎨',
- color: '#3b82f6',
- system: `Du bist der Frontend-Entwickler des IT Nexus Entwicklerteams. Du schreibst den kompletten Frontend-Code.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-- Schreibe vollständige React Komponenten (.jsx Dateien)
-- Glassmorphism Design (konsistent mit bestehendem IT Nexus Style)
-- IMMER api.get/post aus services/api.js verwenden
-- IMMER user.role_name verwenden
-- CSS inline mit var(--*) Variablen
-
-Format pro Datei:
-### Datei: \`frontend/src/pfad/DateiName.jsx\`
-\`\`\`jsx
-// kompletter Code
-\`\`\`
-
-Antworte auf Deutsch für Erklärungen, Code auf Englisch.`
- },
-
- senior: {
- name: 'Senior Dev',
- emoji: '🔍',
- color: '#ef4444',
- system: `Du bist der Senior Developer und Code Reviewer des IT Nexus Entwicklerteams.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-- Reviewe den generierten Code auf Bugs, Sicherheitsprobleme, Konventionsverletzungen
-- Prüfe ob alle Konventionen eingehalten wurden (user.role_name, api.js, asyncHandler, etc.)
-- Prüfe auf fehlende Error Handling
-- Prüfe auf fehlende Route-Registrierungen in server.js
-- Prüfe auf fehlende Sidebar-Links
-- Gib konkrete Fixes wenn nötig
-
-Format:
-✅ Was gut ist
-⚠️ Was fehlt / falsch ist + Fix
-📝 Deploy-Befehl am Ende
-
-Antworte auf Deutsch.`
- },
-
- qa: {
- name: 'QA Engineer',
- emoji: '🧪',
- color: '#8b5cf6',
- system: `Du bist der QA Engineer des IT Nexus Entwicklerteams.
-
-${IT_NEXUS_CONTEXT}
-
-Deine Aufgabe:
-- Identifiziere potenzielle Edge Cases
-- Schreibe manuelle Testschritte die der Entwickler ausführen soll
-- Prüfe ob alle API-Endpoints abgesichert sind (Auth)
-- Prüfe Fehlerszenarien (leere Daten, falscher Input, fehlende Rechte)
-- Schreibe wenn möglich einen Playwright-Test
-
-Format:
-📋 Manuelle Tests (nummeriert)
-🔒 Security-Checks
-⚡ Edge Cases
-(Optional) Playwright Test Code
-
-Antworte auf Deutsch.`
- }
-};
-
-// ─── Anthropic API Aufruf ──────────────────────────────────────────────────────
-function callClaude(systemPrompt, messages, onChunk) {
- return new Promise((resolve, reject) => {
- const body = JSON.stringify({
- model: 'claude-sonnet-4-6',
- max_tokens: 8096,
- system: systemPrompt,
- messages,
- stream: true
- });
-
- const req = https.request({
- hostname: 'api.anthropic.com',
- path: '/v1/messages',
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'x-api-key': API_KEY,
- 'anthropic-version': '2023-06-01',
- 'Content-Length': Buffer.byteLength(body)
- }
- }, (res) => {
- let full = '';
- res.on('data', chunk => {
- const lines = chunk.toString().split('\n');
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- try {
- const d = JSON.parse(line.slice(6));
- if (d.type === 'content_block_delta' && d.delta?.text) {
- full += d.delta.text;
- onChunk(d.delta.text);
- }
- } catch {}
- }
- }
- });
- res.on('end', () => resolve(full));
- res.on('error', reject);
- });
- req.on('error', reject);
- req.write(body);
- req.end();
- });
-}
-
-// ─── Orchestrierung ────────────────────────────────────────────────────────────
-async function runTeam(task, send) {
- const context = [];
-
- const runAgent = async (agentId, userMsg) => {
- const agent = AGENTS[agentId];
- send({ type: 'agent_start', agent: agentId, name: agent.name, emoji: agent.emoji, color: agent.color });
-
- const msgs = [...context, { role: 'user', content: userMsg }];
- let full = '';
- await callClaude(agent.system, msgs, chunk => {
- full += chunk;
- send({ type: 'chunk', agent: agentId, text: chunk });
- });
-
- send({ type: 'agent_done', agent: agentId });
- context.push({ role: 'user', content: userMsg });
- context.push({ role: 'assistant', content: full });
- return full;
- };
-
- // 1. PM analysiert
- const pmResult = await runAgent('pm',
- `Neue Aufgabe für das IT Nexus Entwicklerteam:\n\n"${task}"\n\nErstelle einen kurzen Implementierungsplan.`
- );
-
- // 2. Architect entscheidet
- await runAgent('architect',
- `Aufgabe: "${task}"\n\nPM-Plan:\n${pmResult}\n\nGib deine technischen Entscheidungen für DB-Schema und API-Struktur.`
- );
-
- // 3. Backend Dev schreibt Code
- const backendResult = await runAgent('backend',
- `Aufgabe: "${task}"\n\nSchreibe den vollständigen Backend-Code (Controller, Model, Route, seed.js Migration). Alle Dateien komplett.`
- );
-
- // 4. Frontend Dev schreibt Code
- const frontendResult = await runAgent('frontend',
- `Aufgabe: "${task}"\n\nSchreibe den vollständigen Frontend-Code (Page, Service). Alle Dateien komplett.`
- );
-
- // 5. Senior Dev reviewt
- await runAgent('senior',
- `Reviewe diesen Code:\n\n**Backend:**\n${backendResult}\n\n**Frontend:**\n${frontendResult}\n\nFinde Bugs, Konventionsverletzungen, fehlende Teile.`
- );
-
- // 6. QA testet
- await runAgent('qa',
- `Aufgabe: "${task}"\n\nErstelle Testschritte und prüfe Edge Cases für die implementierte Funktion.`
- );
-
- send({ type: 'done' });
-}
-
-// ─── HTTP Server ───────────────────────────────────────────────────────────────
-const HTML = `
-
-
-
-
-IT Nexus Dev Team
-
-
-
-
-
-
👨💻
-
-
IT Nexus Dev Team
-
6 KI-Agenten · Lokal · Nur für dich
-
-
-
-
-
-
-
-
Dev Team
-
- 📋
- Project Manager
-
-
-
- 🏗️
- Architect
-
-
-
- ⚙️
- Backend Dev
-
-
-
- 🎨
- Frontend Dev
-
-
-
- 🔍
- Senior Dev
-
-
-
- 🧪
- QA Engineer
-
-
-
-
-
-
-
-
🚀
-
Dein persönliches Dev Team
-
Beschreibe was du bauen möchtest. Das Team analysiert, plant, schreibt Code und reviewt alles automatisch.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
⚡ PM → Architect → Backend Dev → Frontend Dev → Senior Dev → QA Engineer
-
-
-
-
-`;
-
-const server = http.createServer((req, res) => {
- const url = new URL(req.url, `http://localhost:${PORT}`);
-
- // HTML
- if (req.method === 'GET' && url.pathname === '/') {
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
- res.end(HTML);
- return;
- }
-
- // Task API
- if (req.method === 'POST' && url.pathname === '/api/task') {
- let body = '';
- req.on('data', c => body += c);
- req.on('end', async () => {
- try {
- const { task, apiKey } = JSON.parse(body);
-
- // API Key aus Request oder Umgebungsvariable
- const key = apiKey || API_KEY;
- if (!key) {
- res.writeHead(400, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: 'Kein API Key' }));
- return;
- }
-
- res.writeHead(200, {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- 'Connection': 'keep-alive',
- 'Access-Control-Allow-Origin': '*'
- });
-
- const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
-
- // Agenten mit aktuellem Key aufrufen
- const callAgent = (systemPrompt, messages) => new Promise((resolve, reject) => {
- const reqBody = JSON.stringify({
- model: 'claude-sonnet-4-6',
- max_tokens: 8096,
- system: systemPrompt,
- messages,
- stream: true
- });
-
- const apiReq = https.request({
- hostname: 'api.anthropic.com',
- path: '/v1/messages',
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'x-api-key': key,
- 'anthropic-version': '2023-06-01',
- 'Content-Length': Buffer.byteLength(reqBody)
- }
- }, (apiRes) => {
- let full = '';
- let buf = '';
- apiRes.on('data', chunk => {
- buf += chunk.toString();
- const lines = buf.split('\n');
- buf = lines.pop();
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- try {
- const d = JSON.parse(line.slice(6));
- if (d.type === 'content_block_delta' && d.delta?.text) {
- full += d.delta.text;
- send({ type: 'chunk', agent: currentAgent, text: d.delta.text });
- }
- } catch {}
- }
- }
- });
- apiRes.on('end', () => resolve(full));
- apiRes.on('error', reject);
- });
- apiReq.on('error', reject);
- apiReq.write(reqBody);
- apiReq.end();
- });
-
- let currentAgent = '';
- const context = [];
-
- const runAgent = async (agentId, userMsg) => {
- currentAgent = agentId;
- send({ type: 'agent_start', agent: agentId });
- const msgs = [...context, { role: 'user', content: userMsg }];
- const full = await callAgent(AGENTS[agentId].system, msgs);
- send({ type: 'agent_done', agent: agentId });
- context.push({ role: 'user', content: userMsg });
- context.push({ role: 'assistant', content: full });
- return full;
- };
-
- // Team ausführen
- const pmResult = await runAgent('pm',
- `Neue Aufgabe:\n\n"${task}"\n\nErstelle einen kurzen klaren Implementierungsplan für das IT Nexus Entwicklerteam.`
- );
-
- await runAgent('architect',
- `Aufgabe: "${task}"\n\nPM-Plan:\n${pmResult}\n\nEntscheide über DB-Schema, API-Endpoints und technische Architektur.`
- );
-
- const backendResult = await runAgent('backend',
- `Aufgabe: "${task}"\n\nSchreibe jetzt den vollständigen Backend-Code. Alle Dateien komplett und lauffähig. Folge exakt den IT Nexus Konventionen.`
- );
-
- const frontendResult = await runAgent('frontend',
- `Aufgabe: "${task}"\n\nSchreibe jetzt den vollständigen Frontend-Code. Alle Dateien komplett. Folge exakt den IT Nexus Konventionen.`
- );
-
- await runAgent('senior',
- `Reviewe diesen Code:\n\nBACKEND:\n${backendResult}\n\nFRONTEND:\n${frontendResult}\n\nFinde alle Bugs, Konventionsverletzungen und fehlende Teile. Gib Fixes.`
- );
-
- await runAgent('qa',
- `Aufgabe: "${task}"\n\nErstelle konkrete Testschritte, Security-Checks und Edge Cases.`
- );
-
- send({ type: 'done' });
- res.end();
- } catch (e) {
- try {
- res.write(`data: ${JSON.stringify({ type: 'error', text: e.message })}\n\n`);
- res.end();
- } catch {}
- }
- });
- return;
- }
-
- res.writeHead(404);
- res.end();
-});
-
-server.listen(PORT, '127.0.0.1', () => {
- console.log('');
- console.log('╔═══════════════════════════════════════╗');
- console.log('║ IT Nexus Dev Team — Bereit! ║');
- console.log('╠═══════════════════════════════════════╣');
- console.log(`║ → http://localhost:${PORT} ║`);
- console.log('║ ║');
- console.log('║ 6 Agenten: PM, Architect, ║');
- console.log('║ Backend, Frontend, Senior, QA ║');
- console.log('╚═══════════════════════════════════════╝');
- console.log('');
- if (!API_KEY) {
- console.log('⚠️ Kein ANTHROPIC_API_KEY gesetzt — im Browser eingeben');
- console.log(' Oder: set ANTHROPIC_API_KEY=sk-ant-... && node devteam.js');
- }
- console.log('');
-});
diff --git a/domain-join-tool/DomainJoinTool.csproj b/domain-join-tool/DomainJoinTool.csproj
deleted file mode 100644
index 8c36810..0000000
--- a/domain-join-tool/DomainJoinTool.csproj
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- WinExe
- net472
- true
- DomainJoinTool
- DomainJoinTool
- app.manifest
- icon.ico
- 9.0
- x64
- true
- none
-
-
-
-
- $(PSHOME)\System.Management.Automation.dll
-
-
-
-
diff --git a/domain-join-tool/MainForm.cs b/domain-join-tool/MainForm.cs
deleted file mode 100644
index a6552bf..0000000
--- a/domain-join-tool/MainForm.cs
+++ /dev/null
@@ -1,642 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Drawing.Text;
-using System.IO;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-using Microsoft.Win32;
-
-namespace DomainJoinTool
-{
- public class MainForm : Form
- {
- // ── Cereda/Zelo Colors ────────────────────────────────────────────────
- static readonly Color CTeal = ColorTranslator.FromHtml("#0D9488");
- static readonly Color CTealD = ColorTranslator.FromHtml("#0F766E");
- static readonly Color CGreen = ColorTranslator.FromHtml("#059669");
- static readonly Color CRed = ColorTranslator.FromHtml("#DC2626");
- static readonly Color CAmber = ColorTranslator.FromHtml("#D97706");
- static readonly Color CGray = ColorTranslator.FromHtml("#6B7280");
- static readonly Color CBG = ColorTranslator.FromHtml("#F8FAFC");
- static readonly Color CBorder= ColorTranslator.FromHtml("#E2E8F0");
- static readonly Color CText = ColorTranslator.FromHtml("#0F172A");
- static readonly Color CText2 = ColorTranslator.FromHtml("#64748B");
-
- // ── State ─────────────────────────────────────────────────────────────
- bool _entra, _domain; string _dn = ""; int _step;
- Timer _animTimer; float _phase, _spin;
-
- // ── Controls ──────────────────────────────────────────────────────────
- StatusBadge _badge;
- Panel _body1, _body2;
- ModernInput _txUser, _txPass;
- RoundButton _btnJoin, _btnLeave, _btnMig;
- Label _lnkBack, _lnkTab1, _lnkTab2;
- ComboBox _cbSrc, _cbDst;
- ProgressBar _pb; Label _lbPb;
- RichTextBox _log;
- Label _lblStat;
- Label _lblMigStatus;
- ProgressBar _pbMig;
- Label _lblMigCount;
- RichTextBox _logMig;
-
- const string DOM = "winkel.local";
- const string LOG = @"C:\ProgramData\DomainJoinTool\join.log";
- const string FLG = @"C:\ProgramData\DomainJoinTool\pending-migration.txt";
- const string EXE = @"C:\ProgramData\DomainJoinTool\DomainJoinTool.exe";
-
- public MainForm()
- {
- Text = "Cereda Systems · Domain Join Tool";
- ClientSize = new Size(620, 680);
- FormBorderStyle = FormBorderStyle.FixedSingle;
- MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen;
- BackColor = CBG; Font = new Font("Segoe UI", 9f); Icon = MakeIcon();
- Build();
- _animTimer = new Timer { Interval = 16 };
- _animTimer.Tick += (s, e) => { _phase = (_phase + 0.025f) % 1f; _spin = (_spin + 9f) % 360f; _badge?.Tick(_phase, _spin); };
- _animTimer.Start();
- DetectStatus();
- LoadProfiles();
- }
-
- void Build()
- {
- // ── Header ────────────────────────────────────────────────────────
- var hdr = new Panel { Dock = DockStyle.Top, Height = 64, BackColor = CTeal };
- hdr.Paint += (s, e) =>
- {
- var g = e.Graphics;
- g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
- g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
- // Subtle darker stripe at bottom
- using (var br = new SolidBrush(Color.FromArgb(30, 0, 0, 0)))
- g.FillRectangle(br, 0, hdr.Height - 3, hdr.Width, 3);
- // Logo circle
- using (var br = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
- g.FillEllipse(br, 14, 10, 36, 36);
- using (var f = new Font("Segoe UI", 11f, FontStyle.Bold))
- using (var br = new SolidBrush(Color.White))
- { var sf = new System.Drawing.StringFormat { Alignment = System.Drawing.StringAlignment.Center, LineAlignment = System.Drawing.StringAlignment.Center }; g.DrawString("IT", f, br, new RectangleF(14, 10, 36, 36), sf); }
- // Text
- using (var br = new SolidBrush(Color.FromArgb(200, 255, 255, 255)))
- g.DrawString("CEREDA SYSTEMS", new Font("Segoe UI", 7.5f, FontStyle.Bold), br, 60, 12);
- g.DrawString("Domain Join Tool", new Font("Segoe UI", 13f, FontStyle.Bold), Brushes.White, 58, 28);
- // Version badge
- using (var br = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
- g.FillRectangle(br, hdr.Width - 68, 20, 56, 20);
- using (var br = new SolidBrush(Color.FromArgb(200, 255, 255, 255)))
- g.DrawString("v2.5.0", new Font("Segoe UI", 8f), br, new RectangleF(hdr.Width - 68, 20, 56, 20),
- new System.Drawing.StringFormat { Alignment = System.Drawing.StringAlignment.Center, LineAlignment = System.Drawing.StringAlignment.Center });
- };
- Controls.Add(hdr);
-
- // ── Tab bar ───────────────────────────────────────────────────────
- var tabBar = new Panel { Dock = DockStyle.Top, Height = 38, BackColor = Color.White };
- tabBar.Paint += (s, e) => {
- e.Graphics.DrawLine(new Pen(CBorder), 0, 37, 600, 37);
- // Active indicator
- if (_step == 0) e.Graphics.FillRectangle(new SolidBrush(CTeal), 16, 35, 120, 3);
- else e.Graphics.FillRectangle(new SolidBrush(CTeal), 148, 35, 140, 3);
- };
- _lnkTab1 = TabLabel("1 · Domain Join", new Point(16, 8), true);
- _lnkTab2 = TabLabel("2 · Profil Migration", new Point(148, 8), false);
- _lnkTab1.Click += (s, e) => GoTo(0, tabBar);
- _lnkTab2.Click += (s, e) => GoTo(1, tabBar);
- tabBar.Controls.Add(_lnkTab1); tabBar.Controls.Add(_lnkTab2);
- Controls.Add(tabBar);
-
- // ── Bodies (explicit position to avoid Dock=Fill overlap bug) ────
- // hdr=64 + tabBar=38 = 102px
- _body1 = new Panel { Location = new Point(0, 102), Size = new Size(620, 578), BackColor = CBG };
- _body2 = new Panel { Location = new Point(0, 102), Size = new Size(620, 578), BackColor = CBG, Visible = false };
- BuildBody1(); BuildBody2();
- Controls.Add(_body1); Controls.Add(_body2);
- }
-
- Label TabLabel(string t, Point loc, bool active)
- {
- var l = new Label { Text = t, Font = new Font("Segoe UI", 9f, active ? FontStyle.Bold : FontStyle.Regular), ForeColor = active ? CTeal : CText2, Location = loc, AutoSize = true, Cursor = Cursors.Hand };
- l.MouseEnter += (s, e) => l.ForeColor = CTeal;
- l.MouseLeave += (s, e) => l.ForeColor = (_step == (l == _lnkTab1 ? 0 : 1)) ? CTeal : CText2;
- return l;
- }
-
- void GoTo(int step, Panel tabBar = null)
- {
- _step = step; _body1.Visible = step == 0; _body2.Visible = step == 1;
- _lnkTab1.Font = new Font("Segoe UI", 9f, step == 0 ? FontStyle.Bold : FontStyle.Regular);
- _lnkTab2.Font = new Font("Segoe UI", 9f, step == 1 ? FontStyle.Bold : FontStyle.Regular);
- _lnkTab1.ForeColor = step == 0 ? CTeal : CText2;
- _lnkTab2.ForeColor = step == 1 ? CTeal : CText2;
- (tabBar ?? _body1.Parent?.Parent as Panel)?.Invalidate();
- // Find and invalidate the tabBar
- foreach (Control c in Controls) if (c is Panel p && p.Height == 38) { p.Invalidate(); break; }
- }
-
- // ── Body 1: Domain Join ───────────────────────────────────────────────
- void BuildBody1()
- {
- int m = 16, y = 14, w = 588;
-
- // ── Status Card ───────────────────────────────────────────────────
- var statCard = MkCard(m, y, w, 86); y += 98;
-
- _lblStat = new Label { Text = "Status", Font = new Font("Segoe UI", 9f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 10), AutoSize = true };
- statCard.Controls.Add(_lblStat);
- statCard.Controls.Add(new Panel { Location = new Point(20, 30), Size = new Size(w - 26, 1), BackColor = CBorder });
-
- _badge = new StatusBadge { Location = new Point(20, 40) };
- statCard.Controls.Add(_badge);
-
- // Right: Entra → AD
- statCard.Controls.Add(new Label { Text = "Entra ID", Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), ForeColor = CText2, Location = new Point(280, 38), AutoSize = true });
- statCard.Controls.Add(new Label { Text = "→", Font = new Font("Segoe UI", 14f), ForeColor = Color.FromArgb(180, 180, 200), Location = new Point(358, 32), AutoSize = true });
- statCard.Controls.Add(new Label { Text = DOM, Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), ForeColor = CGreen, Location = new Point(382, 38), AutoSize = true });
- statCard.Controls.Add(new Label { Text = Environment.MachineName, Font = new Font("Cascadia Mono", 7.5f), ForeColor = CText2, Location = new Point(280, 57), AutoSize = true });
- _body1.Controls.Add(statCard);
-
- // ── Credentials Card ──────────────────────────────────────────────
- var credCard = MkCard(m, y, w, 168); y += 180;
-
- credCard.Controls.Add(new Label { Text = "Domain-Zugangsdaten", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
- credCard.Controls.Add(new Label { Text = "Ziel: " + DOM, Font = new Font("Cascadia Mono", 8f, FontStyle.Bold), ForeColor = CGreen, Location = new Point(220, 15), AutoSize = true });
- credCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
- _txUser = Inp(new Point(20, 44), new Size(w - 40, 52), "Benutzername"); _txUser.Val = "Administrator";
- _txPass = Inp(new Point(20, 104), new Size(w - 40, 52), "Passwort", true);
- credCard.Controls.Add(_txUser); credCard.Controls.Add(_txPass);
- _body1.Controls.Add(credCard);
-
- // ── Action Buttons ────────────────────────────────────────────────
- _btnJoin = new RoundButton("▶ Entra leaven + Domain joinen", CTeal, 10) { Location = new Point(m, y), Size = new Size(420, 44) };
- _btnLeave = new RoundButton("■ Nur Entra verlassen", CRed, 10) { Location = new Point(m + 428, y), Size = new Size(160, 44) };
- _btnJoin.Click += DoJoin; _btnLeave.Click += DoLeave;
- _body1.Controls.Add(_btnJoin); _body1.Controls.Add(_btnLeave); y += 52;
-
- // ── Progress ─────────────────────────────────────────────────────
- _pb = new ProgressBar { Location = new Point(m, y), Size = new Size(w, 4), Style = ProgressBarStyle.Marquee, MarqueeAnimationSpeed = 25, Visible = false }; y += 8;
- _lbPb = Lbl("Bitte warten…", new Point(m, y)); _lbPb.Visible = false; y += 22;
- _body1.Controls.Add(_pb); _body1.Controls.Add(_lbPb);
-
- // ── Log ───────────────────────────────────────────────────────────
- _body1.Controls.Add(Lbl("Log · " + LOG, new Point(m, y), new Font("Cascadia Mono", 7.5f), CText2)); y += 16;
- _log = new RichTextBox { Location = new Point(m, y), Size = new Size(w, 100), BackColor = Color.FromArgb(13, 17, 23), ForeColor = Color.FromArgb(100, 210, 130), Font = new Font("Cascadia Mono", 8f), ReadOnly = true, BorderStyle = BorderStyle.None };
- _body1.Controls.Add(_log);
- }
-
- // ── Body 2: Migration ─────────────────────────────────────────────────
- void BuildBody2()
- {
- int m = 16, y = 14, w = 588;
-
- var infoCard = MkCard(m, y, w, 140); y += 152;
- infoCard.Controls.Add(new Label { Text = "Was wird migriert", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
- infoCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
- int iy = 46;
- foreach (var (t, ok) in new[] {
- ("Dokumente, Desktop, Downloads, Bilder, Videos, Musik", true),
- ("AppData\\Roaming — Outlook, Edge, Chrome, App-Einstellungen", true),
- ("Hintergrundbild, Taskbar-Pins, Startmenü-Layout", true),
- ("Temp-Cache, NTUSER.DAT — nicht übertragen", false),
- }) { infoCard.Controls.Add(Lbl((ok ? "✓ " : "✗ ") + t, new Point(20, iy), null, ok ? CGreen : CAmber)); iy += 24; }
- _body2.Controls.Add(infoCard);
-
- var selCard = MkCard(m, y, w, 108); y += 120;
- selCard.Controls.Add(new Label { Text = "Profile auswählen", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
- selCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
- selCard.Controls.Add(Lbl("Von (altes Entra-Profil):", new Point(20, 46)));
- selCard.Controls.Add(Lbl("Nach (neues Domain-Profil):", new Point(20, 76)));
- _cbSrc = Cmb(new Point(210, 43), new Size(w - 230, 24));
- _cbDst = Cmb(new Point(210, 73), new Size(w - 230, 24));
- selCard.Controls.Add(_cbSrc); selCard.Controls.Add(_cbDst);
- _body2.Controls.Add(selCard);
-
- _btnMig = new RoundButton("▶ Profil jetzt migrieren", CGreen, 10) { Location = new Point(m, y), Size = new Size(440, 44) }; y += 52;
- _btnMig.Click += DoMig;
- _lnkBack = new Label { Text = "← Zurück zu Domain Join", Font = new Font("Segoe UI", 9f), ForeColor = CText2, Location = new Point(m + 448, y - 40), AutoSize = true, Cursor = Cursors.Hand };
- _lnkBack.Click += (s, e) => GoTo(0);
- _lnkBack.MouseEnter += (s, e) => _lnkBack.ForeColor = CTeal;
- _lnkBack.MouseLeave += (s, e) => _lnkBack.ForeColor = CText2;
- _body2.Controls.Add(_btnMig); _body2.Controls.Add(_lnkBack);
-
- // ── Status + Progress + Log (Tab 2) ──────────────────────────────
- _lblMigStatus = new Label { Text = "", Font = new Font("Segoe UI", 9f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(m, y), AutoSize = true };
- _lblMigCount = new Label { Text = "", Font = new Font("Cascadia Mono", 8f), ForeColor = CText2, Location = new Point(m + 300, y), AutoSize = true };
- y += 22;
- _pbMig = new ProgressBar { Location = new Point(m, y), Size = new Size(w, 6), Style = ProgressBarStyle.Marquee, MarqueeAnimationSpeed = 20, Visible = false }; y += 14;
- _logMig = new RichTextBox { Location = new Point(m, y), Size = new Size(w, 106), BackColor = Color.FromArgb(13, 17, 23), ForeColor = Color.FromArgb(100, 210, 130), Font = new Font("Cascadia Mono", 8f), ReadOnly = true, BorderStyle = BorderStyle.None, Visible = false };
- _body2.Controls.Add(_lblMigStatus); _body2.Controls.Add(_lblMigCount);
- _body2.Controls.Add(_pbMig); _body2.Controls.Add(_logMig);
- }
-
- // ── State ─────────────────────────────────────────────────────────────
- void DetectStatus()
- {
- Task.Run(new Action(() => {
- try { var r = Run("dsregcmd", "/status", 10000); _entra = r.Contains("AzureAdJoined : YES"); _domain = r.Contains("DomainJoined : YES"); foreach (var ln in r.Split('\n')) if (ln.Trim().StartsWith("DomainName :")) { _dn = ln.Split(':')[1].Trim(); break; } } catch { }
- Invoke(new Action(() => { RefreshStat(); CheckAutoMig(); }));
- }));
- }
-
- void RefreshStat()
- {
- if (_domain) { _badge.Mode = StatusBadge.BMode.Joined; _lblStat.Text = "Domain joined: " + _dn; _lblStat.ForeColor = CGreen; _btnJoin.Text = "↺ Domain wechseln"; _btnLeave.Text = "■ Verlassen"; }
- else if (_entra) { _badge.Mode = StatusBadge.BMode.Entra; _lblStat.Text = "Entra ID joined — bereit für Domain-Join"; _lblStat.ForeColor = CTealD; }
- else { _badge.Mode = StatusBadge.BMode.None; _lblStat.Text = "Workgroup — kein Domain-Join"; _lblStat.ForeColor = CAmber; _btnLeave.Enabled = false; }
- }
-
- void CheckAutoMig()
- {
- if (!File.Exists(FLG) || !_domain) return;
- try {
- var old = File.ReadAllText(FLG).Trim(); LoadProfiles();
- for (int i = 0; i < _cbSrc.Items.Count; i++) if (_cbSrc.Items[i].ToString().Equals(old, StringComparison.OrdinalIgnoreCase)) { _cbSrc.SelectedIndex = i; break; }
- var cur = Environment.UserName;
- for (int i = 0; i < _cbDst.Items.Count; i++) if (_cbDst.Items[i].ToString().Equals(cur, StringComparison.OrdinalIgnoreCase)) { _cbDst.SelectedIndex = i; break; }
- GoTo(1);
- if (MessageBox.Show("Domain-Join erfolgreich!\n\nProfil migrieren?\n Von: C:\\Users\\" + old + "\n Nach: C:\\Users\\" + cur, "Profil Migration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
- DoMig(null, EventArgs.Empty);
- else File.Delete(FLG);
- } catch { }
- }
-
- void LoadProfiles()
- {
- if (_cbSrc == null) return;
- var list = new List();
- if (Directory.Exists(@"C:\Users")) foreach (var d in Directory.GetDirectories(@"C:\Users")) { var n = Path.GetFileName(d); if (n != "Public" && n != "Default" && n != "Default User" && n != "All Users") list.Add(n); }
- _cbSrc.Items.Clear(); _cbDst.Items.Clear();
- foreach (var p in list) { _cbSrc.Items.Add(p); _cbDst.Items.Add(p); }
- if (_cbSrc.Items.Count > 0) _cbSrc.SelectedIndex = 0;
- if (_cbDst.Items.Count > 1) _cbDst.SelectedIndex = 1;
- }
-
- // ── Actions ───────────────────────────────────────────────────────────
- async void DoJoin(object s, EventArgs e)
- {
- if (string.IsNullOrWhiteSpace(_txUser.Val) || string.IsNullOrWhiteSpace(_txPass.Val)) { MessageBox.Show("Bitte Benutzername und Passwort eingeben.", "Eingabe fehlt", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
- if (MessageBox.Show("Computer wird der Domain '" + DOM + "' beigetreten.\n\nFortfahren?", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
- Busy(true, "Domain-Wechsel läuft…"); _badge.Mode = StatusBadge.BMode.Working;
- var user = _txUser.Val.Trim(); var pass = _txPass.Val;
- await Task.Run(new Action(async () => {
- if (_entra) { Prog("Entra ID verlassen…"); Log(Run("dsregcmd", "/leave", 30000)); await Task.Delay(2000); }
- Prog("Domain joinen…");
- if (!user.Contains("\\") && !user.Contains("@")) user = DOM + "\\" + user;
- var res = PS("$pw=ConvertTo-SecureString '" + Esc(pass) + "' -AsPlainText -Force; $cr=New-Object System.Management.Automation.PSCredential('" + Esc(user) + "',$pw); Add-Computer -DomainName '" + DOM + "' -Credential $cr -Force 2>&1");
- Log(res);
- var ok = !res.ToLower().Contains("fehler") && !res.ToLower().Contains("error") && !res.ToLower().Contains("failed") && !res.ToLower().Contains("denied");
- if (ok) SetupAutoMig();
- Invoke(new Action(() => {
- Busy(false, "");
- if (ok) { if (MessageBox.Show("Erfolgreich!\n\nJetzt neu starten?", "✓ Fertig", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) Run("shutdown", "/r /t 5 /c \"Domain Join\""); }
- else MessageBox.Show("Fehlgeschlagen — Log prüfen.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
- DetectStatus();
- }));
- }));
- }
-
- async void DoLeave(object s, EventArgs e)
- {
- if (MessageBox.Show("Computer verlässt " + (_domain ? "Domain '" + _dn + "'" : "Entra ID") + ".\n\nFortfahren?", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
- Busy(true, "Verlasse…"); _badge.Mode = StatusBadge.BMode.Working;
- await Task.Run(new Action(() => { Log(Run("dsregcmd", "/leave", 30000)); Invoke(new Action(() => { Busy(false, ""); MessageBox.Show("Fertig. Bitte neu starten.", "Erledigt", MessageBoxButtons.OK, MessageBoxIcon.Information); DetectStatus(); })); }));
- }
-
- void SetupAutoMig()
- {
- try { Directory.CreateDirectory(Path.GetDirectoryName(FLG)); File.WriteAllText(FLG, Environment.UserName); File.Copy(System.Reflection.Assembly.GetExecutingAssembly().Location, EXE, true); using (var k = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", true)) k.SetValue("DomainJoinMigration", "\"" + EXE + "\""); Log("[AUTO] Migration startet nach nächstem Login automatisch"); } catch (Exception ex) { Log("[WARN] " + ex.Message); }
- }
-
- async void DoMig(object s, EventArgs e)
- {
- if (_cbSrc.SelectedItem == null || _cbDst.SelectedItem == null)
- {
- MessageBox.Show("Bitte Quell- und Zielprofil auswählen.", "Profil fehlt", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- return;
- }
- var src = _cbSrc.SelectedItem.ToString();
- var dst = _cbDst.SelectedItem.ToString();
- if (src == dst) { MessageBox.Show("Quelle und Ziel sind identisch.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
- var srcPath = @"C:\Users\" + src;
- var dstPath = @"C:\Users\" + dst;
- if (!Directory.Exists(srcPath)) { MessageBox.Show("Quellprofil nicht gefunden:\n" + srcPath, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); return; }
- if (MessageBox.Show("Profil kopieren?\n\nVon: " + srcPath + "\nNach: " + dstPath + "\n\nDas kann mehrere Minuten dauern.", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
-
- _btnMig.Enabled = false;
- _logMig.Visible = true;
- _logMig.Clear();
- _pbMig.Visible = true;
- _lblMigCount.Text = "";
- MigLog("[START] " + src + " → " + dst);
- MigStatus("⏳ Migration läuft…");
- Log("[START] Profil-Migration: " + src + " → " + dst);
-
- await Task.Run(new Action(() =>
- {
- try
- {
- if (!Directory.Exists(dstPath))
- {
- Directory.CreateDirectory(dstPath);
- MigLog("[INFO] Zielordner angelegt: " + dstPath);
- }
-
- var args = "\"" + srcPath + "\" \"" + dstPath + "\""
- + " /E /XJ /XC /XN /XO"
- + " /XA:O"
- + " /XD Temp INetCache WebCache \"AppData\\Local\\Temp\" OneDrive \"Cereda Systems GmbH\" \"OneDrive - Cereda Systems GmbH\""
- + " /XF NTUSER.DAT ntuser.dat ntuser.ini \"*.regtrans-ms\" \"*.blf\""
- + " /R:0 /W:0 /NP";
-
- MigLog("[CMD] robocopy " + args);
- var psi = new System.Diagnostics.ProcessStartInfo(
- @"C:\Windows\System32\robocopy.exe", args)
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true
- };
- int exitCode = -1;
- int copiedFiles = 0, skippedFiles = 0;
- using (var proc = System.Diagnostics.Process.Start(psi))
- {
- string line;
- while ((line = proc.StandardOutput.ReadLine()) != null)
- {
- MigLog(line);
- // Count copied files ("Neue Datei" or "New File")
- if (line.Contains("Neue Datei") || line.Contains("New File"))
- {
- copiedFiles++;
- MigCount("Kopiert: " + copiedFiles + " Dateien" + (skippedFiles > 0 ? " • Übersprungen: " + skippedFiles : ""));
- }
- else if (line.Contains("FEHLER") || line.Contains("ERROR"))
- {
- skippedFiles++;
- MigCount("Kopiert: " + copiedFiles + " Dateien • Übersprungen: " + skippedFiles);
- }
- }
- proc.WaitForExit(1800000);
- exitCode = proc.ExitCode;
- }
- Invoke(new Action(() => _pbMig.Visible = false));
- // robocopy: 0-7 = OK (0=nichts zu tun, 1=kopiert, 2=extra, etc.), 8+ = Fehler
- if (exitCode >= 8)
- {
- MigLog("[FEHLER] ExitCode=" + exitCode);
- MigStatus("✗ Fehler (ExitCode " + exitCode + ")");
- }
- else
- {
- MigLog("[OK] Abgeschlossen. ExitCode=" + exitCode);
- MigStatus("✓ Migration abgeschlossen — " + copiedFiles + " Dateien kopiert");
- }
-
- try { if (File.Exists(FLG)) File.Delete(FLG); } catch { }
-
- Invoke(new Action(() =>
- {
- _btnMig.Enabled = true;
- MessageBox.Show("Migration abgeschlossen!\n\nDateien sind in:\n" + dstPath,
- "✓ Fertig", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }));
- }
- catch (Exception ex)
- {
- MigLog("[EXCEPTION] " + ex.Message);
- MigStatus("✗ Fehler aufgetreten");
- Log("[EXCEPTION] Migration: " + ex.Message);
- Invoke(new Action(() =>
- {
- _btnMig.Enabled = true;
- MessageBox.Show("Fehler bei Migration:\n\n" + ex.Message, "Fehler",
- MessageBoxButtons.OK, MessageBoxIcon.Error);
- }));
- }
- }));
- }
-
- // ── Helpers ───────────────────────────────────────────────────────────
- void Busy(bool b, string m) { if (InvokeRequired) { Invoke(new Action(() => Busy(b, m))); return; } _btnJoin.Enabled = _btnLeave.Enabled = !b; _pb.Visible = _lbPb.Visible = b; if (!string.IsNullOrEmpty(m)) _lbPb.Text = m; }
- void Prog(string m) { if (InvokeRequired) { Invoke(new Action(() => Prog(m))); return; } _lbPb.Text = m; Log(m); }
- void Log(string t) { if (InvokeRequired) { Invoke(new Action(() => Log(t))); return; } if (string.IsNullOrWhiteSpace(t)) return; var ln = "[" + DateTime.Now.ToString("HH:mm:ss") + "] " + t.Trim(); _log?.AppendText(ln + "\n"); _log?.ScrollToCaret(); try { Directory.CreateDirectory(Path.GetDirectoryName(LOG)); File.AppendAllText(LOG, DateTime.Now.ToString("yyyy-MM-dd") + " " + ln + Environment.NewLine); } catch { } }
- void MigLog(string t) { if (InvokeRequired) { Invoke(new Action(() => MigLog(t))); return; } if (string.IsNullOrWhiteSpace(t)) return; _logMig?.AppendText(t.TrimEnd() + "\n"); _logMig?.ScrollToCaret(); }
- void MigStatus(string t) { if (InvokeRequired) { Invoke(new Action(() => MigStatus(t))); return; } if (_lblMigStatus != null) { _lblMigStatus.Text = t; _lblMigStatus.ForeColor = t.StartsWith("✓") ? CGreen : t.StartsWith("✗") ? CRed : CTealD; } }
- void MigCount(string t) { if (InvokeRequired) { Invoke(new Action(() => MigCount(t))); return; } if (_lblMigCount != null) _lblMigCount.Text = t; }
-
- // Same pattern as ZeloServerVerwaltung
- Panel MkCard(int x, int y, int w, int h)
- {
- var p = new Panel { Location = new Point(x, y), Size = new Size(w, h), BackColor = Color.White };
- var accent = new Panel { Location = Point.Empty, Size = new Size(5, h), BackColor = CTeal };
- p.Controls.Add(accent);
- p.Paint += (s, e) =>
- {
- var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
- // Border
- using (var path = RR(1, 1, w - 2, h - 2, 10))
- using (var pen = new Pen(Color.FromArgb(215, 225, 235), 1f)) g.DrawPath(pen, path);
- // Subtle bottom shadow line
- using (var pen = new Pen(Color.FromArgb(50, 0, 100, 150), 1.5f))
- g.DrawLine(pen, 10, h - 1, w - 4, h - 1);
- };
- using (var path = RR(0, 0, w, h, 10))
- p.Region = new Region(path);
- return p;
- }
-
- Label Lbl(string t, Point loc, Font f = null, Color? c = null) => new Label { Text = t, Font = f ?? new Font("Segoe UI", 9f), ForeColor = c ?? CText2, Location = loc, AutoSize = true };
- ModernInput Inp(Point loc, Size sz, string placeholder = "", bool pw = false)
- {
- return new ModernInput(placeholder, pw) { Location = loc, Size = sz };
- }
- ComboBox Cmb(Point loc, Size sz) => new ComboBox { Location = loc, Size = sz, DropDownStyle = ComboBoxStyle.DropDownList, FlatStyle = FlatStyle.Flat, Font = new Font("Segoe UI", 9f) };
-
- static GraphicsPath RR(int x, int y, int w, int h, int r) { var p = new GraphicsPath(); p.AddArc(x, y, r * 2, r * 2, 180, 90); p.AddArc(x + w - r * 2, y, r * 2, r * 2, 270, 90); p.AddArc(x + w - r * 2, y + h - r * 2, r * 2, r * 2, 0, 90); p.AddArc(x, y + h - r * 2, r * 2, r * 2, 90, 90); p.CloseFigure(); return p; }
- static Icon MakeIcon()
- {
- var bmp = new Bitmap(32, 32);
- using (var g = Graphics.FromImage(bmp))
- {
- g.SmoothingMode = SmoothingMode.AntiAlias;
- g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
- // Teal rounded square
- using (var path = RR(0, 0, 32, 32, 6))
- using (var br = new SolidBrush(ColorTranslator.FromHtml("#0D9488")))
- g.FillPath(br, path);
- // White "C" letter
- var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
- g.DrawString("C", new Font("Segoe UI", 16f, FontStyle.Bold), Brushes.White, new RectangleF(0, 0, 32, 32), sf);
- }
- IntPtr hIcon = bmp.GetHicon();
- return Icon.FromHandle(hIcon);
- }
-
- string Run(string exe, string args, int ms = 60000) { var psi = new ProcessStartInfo(exe, args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (var p = Process.Start(psi)) { var o = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd(); p.WaitForExit(ms); return o; } }
- string PS(string sc) => Run("powershell.exe", "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"" + sc.Replace("\"", "\\\"") + "\"", 120000);
- static string Esc(string s) => s.Replace("'", "''");
-
- protected override void OnFormClosed(FormClosedEventArgs e) { _animTimer?.Stop(); _animTimer?.Dispose(); base.OnFormClosed(e); }
-
- // ── ModernInput — Floating-Label + Fokus-Effekt + Validierung ─────────
- class ModernInput : Panel
- {
- readonly Label _lbl;
- readonly TextBox _txt;
- readonly Label _icon;
- bool _focused, _ready;
- static readonly Color Teal = ColorTranslator.FromHtml("#0D9488");
- static readonly Color Gray = ColorTranslator.FromHtml("#94A3B8");
- static readonly Color Border = ColorTranslator.FromHtml("#E2E8F0");
- static readonly Color Green = ColorTranslator.FromHtml("#059669");
- static readonly Color Red = ColorTranslator.FromHtml("#DC2626");
-
- public string Val { get => _txt.Text; set { _txt.Text = value; Update_(); } }
-
- public ModernInput(string placeholder, bool pw = false)
- {
- DoubleBuffered = true;
- SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint |
- ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
- BackColor = Color.Transparent; Height = 52;
-
- _lbl = new Label {
- Text = placeholder,
- Font = new Font("Segoe UI", 9.5f),
- ForeColor = Gray,
- Location = new Point(13, 15),
- AutoSize = true
- };
-
- _txt = new TextBox {
- BorderStyle = BorderStyle.None,
- Location = new Point(13, 28),
- Font = new Font("Segoe UI", 10.5f),
- BackColor = Color.White
- };
- if (pw) _txt.PasswordChar = '●';
-
- // Validation icon
- _icon = new Label {
- Font = new Font("Segoe UI", 12f),
- Text = "",
- AutoSize = true,
- ForeColor = Gray
- };
-
- _txt.GotFocus += (s, e) => { _focused = true; Update_(); Invalidate(); };
- _txt.LostFocus += (s, e) => { _focused = false; Update_(); Invalidate(); };
- _txt.TextChanged += (s, e) => { Update_(); Invalidate(); };
-
- Controls.Add(_lbl);
- Controls.Add(_txt);
- Controls.Add(_icon);
- _ready = true;
- }
-
- void Update_()
- {
- bool fl = _focused || !string.IsNullOrEmpty(_txt.Text);
- _lbl.Font = new Font("Segoe UI", fl ? 7.5f : 9.5f);
- _lbl.Location = new Point(13, fl ? 6 : 15);
- _lbl.ForeColor = _focused ? Teal : Gray;
-
- bool filled = !string.IsNullOrWhiteSpace(_txt.Text);
- _icon.Text = filled ? "✓" : "";
- _icon.ForeColor = filled ? Green : Gray;
- if (_icon.Width > 0)
- _icon.Location = new Point(Width - _icon.Width - 12, (Height - _icon.Height) / 2);
- }
-
- protected override void OnResize(EventArgs e)
- {
- base.OnResize(e);
- if (!_ready || _txt == null) return;
- _txt.Width = Width - 32;
- Update_();
- }
-
- protected override void OnPaint(PaintEventArgs e)
- {
- var g = e.Graphics;
- g.SmoothingMode = SmoothingMode.AntiAlias;
-
- // White background
- using (var br = new SolidBrush(Color.White))
- g.FillRectangle(br, 0, 0, Width, Height);
-
- // Border
- var bc = _focused ? Teal : Border;
- using (var pen = new Pen(bc, _focused ? 2f : 1f))
- g.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
-
- // Bottom accent line on focus
- if (_focused)
- using (var pen = new Pen(Teal, 2.5f))
- g.DrawLine(pen, 2, Height - 1, Width - 3, Height - 1);
- }
- }
-
- // ── RoundButton (same as ZeloServerVerwaltung) ────────────────────────
- class RoundButton : Control
- {
- bool _hov, _press; readonly Color _base, _hov2, _press2; readonly int _r;
- public RoundButton(string text, Color baseColor, int radius = 8)
- {
- _base = baseColor; _hov2 = ControlPaint.Dark(baseColor, 0.10f); _press2 = ControlPaint.Dark(baseColor, 0.20f); _r = radius;
- Text = text; Font = new Font("Segoe UI", 9f, FontStyle.Bold); Cursor = Cursors.Hand;
- SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
- }
- protected override void OnPaint(PaintEventArgs e)
- {
- var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
- g.Clear(Parent?.BackColor ?? Color.White);
- var fill = _press ? _press2 : (_hov && Enabled) ? _hov2 : _base;
- using (var path = RR(0, 0, Width, Height, _r)) using (var br = new SolidBrush(fill)) g.FillPath(br, path);
- if (!Enabled) using (var path = RR(0, 0, Width, Height, _r)) using (var br = new SolidBrush(Color.FromArgb(110, 255, 255, 255))) g.FillPath(br, path);
- TextRenderer.DrawText(e.Graphics, Text, Font, new Rectangle(0, 0, Width, Height), Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
- }
- protected override void OnMouseEnter(EventArgs e) { _hov = true; Invalidate(); base.OnMouseEnter(e); }
- protected override void OnMouseLeave(EventArgs e) { _hov = false; _press = false; Invalidate(); base.OnMouseLeave(e); }
- protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _press = true; Invalidate(); } base.OnMouseDown(e); }
- protected override void OnMouseUp(MouseEventArgs e) { _press = false; Invalidate(); base.OnMouseUp(e); }
- protected override void OnEnabledChanged(EventArgs e) { Cursor = Enabled ? Cursors.Hand : Cursors.Default; Invalidate(); base.OnEnabledChanged(e); }
- }
-
- // ── StatusBadge (adapted from ZeloServerVerwaltung) ───────────────────
- class StatusBadge : Control
- {
- public enum BMode { Entra, Joined, None, Working }
- BMode _mode; float _phase, _spin;
- public BMode Mode { get => _mode; set { _mode = value; Invalidate(); } }
- public StatusBadge() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); Size = new Size(230, 28); BackColor = Color.White; }
- public void Tick(float ph, float sp) { _phase = ph; _spin = sp; if (_mode != BMode.None) Invalidate(); }
- protected override void OnPaint(PaintEventArgs e)
- {
- var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; g.Clear(Color.White);
- Color bg; string txt;
- switch (_mode) {
- case BMode.Joined: bg = ColorTranslator.FromHtml("#059669"); txt = "DOMAIN JOINED"; break;
- case BMode.Entra: bg = ColorTranslator.FromHtml("#0D9488"); txt = "ENTRA ID JOINED"; break;
- case BMode.Working: bg = ColorTranslator.FromHtml("#D97706"); txt = "BITTE WARTEN..."; break;
- default: bg = ColorTranslator.FromHtml("#6B7280"); txt = "WORKGROUP"; break;
- }
- int r = Height / 2;
- using (var path = new GraphicsPath()) { path.AddArc(0, 0, r * 2, r * 2, 90, 180); path.AddArc(Width - r * 2, 0, r * 2, r * 2, 270, 180); path.CloseFigure(); using (var br = new SolidBrush(bg)) g.FillPath(br, path); }
- // Dot
- int dx = r, dy = r, dr = 5;
- if (_mode == BMode.Working) { using (var p2 = new Pen(Color.FromArgb(60, Color.White), 2f)) g.DrawEllipse(p2, dx - dr, dy - dr, dr * 2, dr * 2); using (var p2 = new Pen(Color.White, 2f) { StartCap = LineCap.Round, EndCap = LineCap.Round }) g.DrawArc(p2, dx - dr, dy - dr, dr * 2, dr * 2, _spin, 100f); }
- else { double pv = Math.Sin(_phase * Math.PI * 2) * 0.5 + 0.5; int gr = dr + (int)(pv * 2); using (var b = new SolidBrush(Color.FromArgb(60, Color.White))) g.FillEllipse(b, dx - gr, dy - gr, gr * 2, gr * 2); using (var b = new SolidBrush(Color.White)) g.FillEllipse(b, dx - dr, dy - dr, dr * 2, dr * 2); }
- // Text
- var rect = new RectangleF(r * 2 + 6, 0, Width - r * 2 - 6 - r, Height);
- var sf = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center };
- using (var f = new Font("Segoe UI", 8.5f, FontStyle.Bold)) using (var br = new SolidBrush(Color.White)) g.DrawString(txt, f, br, rect, sf);
- }
- }
- }
-}
diff --git a/domain-join-tool/Program.cs b/domain-join-tool/Program.cs
deleted file mode 100644
index cb2e667..0000000
--- a/domain-join-tool/Program.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Windows.Forms;
-
-namespace DomainJoinTool
-{
- internal static class Program
- {
- [STAThread]
- static void Main()
- {
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
- Application.ThreadException += (s, e) =>
- System.IO.File.AppendAllText(@"C:\ProgramData\DomainJoinTool\crash.log",
- DateTime.Now + ": " + e.Exception + Environment.NewLine);
- AppDomain.CurrentDomain.UnhandledException += (s, e) =>
- System.IO.File.AppendAllText(@"C:\ProgramData\DomainJoinTool\crash.log",
- DateTime.Now + ": " + e.ExceptionObject + Environment.NewLine);
- try
- {
- Application.Run(new MainForm());
- }
- catch (Exception ex)
- {
- MessageBox.Show("Startfehler: " + ex.Message + "\n\n" + ex.StackTrace, "Fehler");
- }
- }
- }
-}
diff --git a/domain-join-tool/app.manifest b/domain-join-tool/app.manifest
deleted file mode 100644
index feec5a1..0000000
--- a/domain-join-tool/app.manifest
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/domain-join-tool/bin/Release/net472/DomainJoinTool.exe.config b/domain-join-tool/bin/Release/net472/DomainJoinTool.exe.config
deleted file mode 100644
index 8f60dcb..0000000
--- a/domain-join-tool/bin/Release/net472/DomainJoinTool.exe.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/domain-join-tool/icon.ico b/domain-join-tool/icon.ico
deleted file mode 100644
index 3717d30..0000000
Binary files a/domain-join-tool/icon.ico and /dev/null differ
diff --git a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.dgspec.json b/domain-join-tool/obj/DomainJoinTool.csproj.nuget.dgspec.json
deleted file mode 100644
index f85b209..0000000
--- a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.dgspec.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "format": 1,
- "restore": {
- "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj": {}
- },
- "projects": {
- "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj": {
- "version": "1.0.0",
- "restore": {
- "projectUniqueName": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
- "projectName": "DomainJoinTool",
- "projectPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
- "packagesPath": "C:\\Users\\gruessing\\.nuget\\packages\\",
- "outputPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\obj\\",
- "projectStyle": "PackageReference",
- "configFilePaths": [
- "C:\\Users\\gruessing\\AppData\\Roaming\\NuGet\\NuGet.Config"
- ],
- "originalTargetFrameworks": [
- "net472"
- ],
- "sources": {
- "https://api.nuget.org/v3/index.json": {}
- },
- "frameworks": {
- "net472": {
- "targetAlias": "net472",
- "projectReferences": {}
- }
- },
- "warningProperties": {
- "warnAsError": [
- "NU1605"
- ]
- },
- "restoreAuditProperties": {
- "enableAudit": "true",
- "auditLevel": "low",
- "auditMode": "direct"
- }
- },
- "frameworks": {
- "net472": {
- "targetAlias": "net472",
- "dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies": {
- "suppressParent": "All",
- "target": "Package",
- "version": "[1.0.3, )",
- "autoReferenced": true
- }
- },
- "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.421\\RuntimeIdentifierGraph.json"
- }
- },
- "runtimes": {
- "win7-x64": {
- "#import": []
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.props b/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.props
deleted file mode 100644
index 5bbd9e7..0000000
--- a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.props
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- True
- NuGet
- $(MSBuildThisFileDirectory)project.assets.json
- $(UserProfile)\.nuget\packages\
- C:\Users\gruessing\.nuget\packages\
- PackageReference
- 6.11.2
-
-
-
-
-
\ No newline at end of file
diff --git a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.targets b/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.targets
deleted file mode 100644
index c7f69cc..0000000
--- a/domain-join-tool/obj/DomainJoinTool.csproj.nuget.g.targets
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/domain-join-tool/obj/Release/net472/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs b/domain-join-tool/obj/Release/net472/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs
deleted file mode 100644
index 3871b18..0000000
--- a/domain-join-tool/obj/Release/net472/.NETFramework,Version=v4.7.2.AssemblyAttributes.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-//
-using System;
-using System.Reflection;
-[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfo.cs b/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfo.cs
deleted file mode 100644
index 6d63e63..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-using System;
-using System.Reflection;
-
-[assembly: System.Reflection.AssemblyCompanyAttribute("DomainJoinTool")]
-[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
-[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
-[assembly: System.Reflection.AssemblyProductAttribute("DomainJoinTool")]
-[assembly: System.Reflection.AssemblyTitleAttribute("DomainJoinTool")]
-[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
-
-// Von der MSBuild WriteCodeFragment-Klasse generiert.
-
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfoInputs.cache b/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfoInputs.cache
deleted file mode 100644
index 90a3e8f..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.AssemblyInfoInputs.cache
+++ /dev/null
@@ -1 +0,0 @@
-d0c739f07a28fc8609fc4a098983a9d7bd13f38d051a4df2bc76519c63de7a08
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.GeneratedMSBuildEditorConfig.editorconfig b/domain-join-tool/obj/Release/net472/DomainJoinTool.GeneratedMSBuildEditorConfig.editorconfig
deleted file mode 100644
index 727ee35..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.GeneratedMSBuildEditorConfig.editorconfig
+++ /dev/null
@@ -1,11 +0,0 @@
-is_global = true
-build_property.ApplicationManifest = app.manifest
-build_property.StartupObject =
-build_property.ApplicationDefaultFont =
-build_property.ApplicationHighDpiMode =
-build_property.ApplicationUseCompatibleTextRendering =
-build_property.ApplicationVisualStyles =
-build_property.RootNamespace = DomainJoinTool
-build_property.ProjectDir = C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\
-build_property.EnableComHosting =
-build_property.EnableGeneratedComInterfaceComImportInterop =
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.assets.cache b/domain-join-tool/obj/Release/net472/DomainJoinTool.assets.cache
deleted file mode 100644
index 292bafc..0000000
Binary files a/domain-join-tool/obj/Release/net472/DomainJoinTool.assets.cache and /dev/null differ
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.AssemblyReference.cache b/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.AssemblyReference.cache
deleted file mode 100644
index dd3168d..0000000
Binary files a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.AssemblyReference.cache and /dev/null differ
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.CoreCompileInputs.cache b/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.CoreCompileInputs.cache
deleted file mode 100644
index 52b0f4f..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.CoreCompileInputs.cache
+++ /dev/null
@@ -1 +0,0 @@
-074cd1570727f0b97007ce35e18f8b37cb149bef28991a72fe6b298b163c25a9
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.FileListAbsolute.txt b/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.FileListAbsolute.txt
deleted file mode 100644
index dd822b0..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.csproj.FileListAbsolute.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.csproj.AssemblyReference.cache
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.GeneratedMSBuildEditorConfig.editorconfig
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.AssemblyInfoInputs.cache
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.AssemblyInfo.cs
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.csproj.CoreCompileInputs.cache
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\bin\Release\net472\DomainJoinTool.exe.config
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\bin\Release\net472\DomainJoinTool.exe
-C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.exe
diff --git a/domain-join-tool/obj/Release/net472/DomainJoinTool.exe.withSupportedRuntime.config b/domain-join-tool/obj/Release/net472/DomainJoinTool.exe.withSupportedRuntime.config
deleted file mode 100644
index 8f60dcb..0000000
--- a/domain-join-tool/obj/Release/net472/DomainJoinTool.exe.withSupportedRuntime.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/domain-join-tool/obj/project.assets.json b/domain-join-tool/obj/project.assets.json
deleted file mode 100644
index 8a5a2a6..0000000
--- a/domain-join-tool/obj/project.assets.json
+++ /dev/null
@@ -1,489 +0,0 @@
-{
- "version": 3,
- "targets": {
- ".NETFramework,Version=v4.7.2": {
- "Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
- "type": "package",
- "dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
- }
- },
- "Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
- "type": "package",
- "build": {
- "build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets": {}
- }
- }
- },
- ".NETFramework,Version=v4.7.2/win7-x64": {
- "Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
- "type": "package",
- "dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
- }
- },
- "Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
- "type": "package",
- "build": {
- "build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets": {}
- }
- }
- }
- },
- "libraries": {
- "Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
- "sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
- "type": "package",
- "path": "microsoft.netframework.referenceassemblies/1.0.3",
- "files": [
- ".nupkg.metadata",
- ".signature.p7s",
- "microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
- "microsoft.netframework.referenceassemblies.nuspec"
- ]
- },
- "Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
- "sha512": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw==",
- "type": "package",
- "path": "microsoft.netframework.referenceassemblies.net472/1.0.3",
- "files": [
- ".nupkg.metadata",
- ".signature.p7s",
- "build/.NETFramework/v4.7.2/Accessibility.dll",
- "build/.NETFramework/v4.7.2/Accessibility.xml",
- "build/.NETFramework/v4.7.2/CustomMarshalers.dll",
- "build/.NETFramework/v4.7.2/CustomMarshalers.xml",
- "build/.NETFramework/v4.7.2/Facades/Microsoft.Win32.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.AppContext.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Collections.Concurrent.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Collections.NonGeneric.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Collections.Specialized.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Collections.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ComponentModel.Annotations.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ComponentModel.EventBasedAsync.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ComponentModel.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ComponentModel.TypeConverter.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ComponentModel.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Console.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Data.Common.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Contracts.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Debug.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.FileVersionInfo.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Process.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.StackTrace.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.TextWriterTraceListener.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Tools.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Diagnostics.TraceSource.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Drawing.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Dynamic.Runtime.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Globalization.Calendars.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Globalization.Extensions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Globalization.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.Compression.ZipFile.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.DriveInfo.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.Watcher.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.IsolatedStorage.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.MemoryMappedFiles.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.Pipes.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.UnmanagedMemoryStream.dll",
- "build/.NETFramework/v4.7.2/Facades/System.IO.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Linq.Expressions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Linq.Parallel.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Linq.Queryable.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Linq.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Http.Rtc.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.NameResolution.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.NetworkInformation.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Ping.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Requests.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Security.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.Sockets.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.WebHeaderCollection.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.WebSockets.Client.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Net.WebSockets.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ObjectModel.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.ILGeneration.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.Lightweight.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.Extensions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Reflection.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Resources.Reader.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Resources.ResourceManager.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Resources.Writer.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.CompilerServices.VisualC.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Extensions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Handles.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Numerics.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Formatters.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Json.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Xml.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Runtime.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Claims.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Algorithms.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Csp.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Encoding.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.X509Certificates.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.Principal.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Security.SecureString.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Duplex.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Http.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ServiceModel.NetTcp.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Primitives.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Security.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Text.Encoding.Extensions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Text.Encoding.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Text.RegularExpressions.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.Overlapped.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.Tasks.Parallel.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.Tasks.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.Thread.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.ThreadPool.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.Timer.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Threading.dll",
- "build/.NETFramework/v4.7.2/Facades/System.ValueTuple.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.ReaderWriter.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.XDocument.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.XPath.XDocument.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.XPath.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.XmlDocument.dll",
- "build/.NETFramework/v4.7.2/Facades/System.Xml.XmlSerializer.dll",
- "build/.NETFramework/v4.7.2/Facades/netstandard.dll",
- "build/.NETFramework/v4.7.2/ISymWrapper.dll",
- "build/.NETFramework/v4.7.2/ISymWrapper.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Activities.Build.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Activities.Build.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Conversion.v4.0.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Conversion.v4.0.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Engine.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Engine.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Framework.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Framework.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Tasks.v4.0.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Tasks.v4.0.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Utilities.v4.0.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.Utilities.v4.0.xml",
- "build/.NETFramework/v4.7.2/Microsoft.Build.dll",
- "build/.NETFramework/v4.7.2/Microsoft.Build.xml",
- "build/.NETFramework/v4.7.2/Microsoft.CSharp.dll",
- "build/.NETFramework/v4.7.2/Microsoft.CSharp.xml",
- "build/.NETFramework/v4.7.2/Microsoft.JScript.dll",
- "build/.NETFramework/v4.7.2/Microsoft.JScript.xml",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.Data.dll",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.Data.xml",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.dll",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.xml",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.dll",
- "build/.NETFramework/v4.7.2/Microsoft.VisualBasic.xml",
- "build/.NETFramework/v4.7.2/Microsoft.VisualC.STLCLR.dll",
- "build/.NETFramework/v4.7.2/Microsoft.VisualC.STLCLR.xml",
- "build/.NETFramework/v4.7.2/Microsoft.VisualC.dll",
- "build/.NETFramework/v4.7.2/Microsoft.VisualC.xml",
- "build/.NETFramework/v4.7.2/PermissionSets/FullTrust.xml",
- "build/.NETFramework/v4.7.2/PermissionSets/Internet.xml",
- "build/.NETFramework/v4.7.2/PermissionSets/LocalIntranet.xml",
- "build/.NETFramework/v4.7.2/PresentationBuildTasks.dll",
- "build/.NETFramework/v4.7.2/PresentationBuildTasks.xml",
- "build/.NETFramework/v4.7.2/PresentationCore.dll",
- "build/.NETFramework/v4.7.2/PresentationCore.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.Aero.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.Aero.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.Aero2.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.Aero2.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.AeroLite.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.AeroLite.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.Classic.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.Classic.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.Luna.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.Luna.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.Royale.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.Royale.xml",
- "build/.NETFramework/v4.7.2/PresentationFramework.dll",
- "build/.NETFramework/v4.7.2/PresentationFramework.xml",
- "build/.NETFramework/v4.7.2/ReachFramework.dll",
- "build/.NETFramework/v4.7.2/ReachFramework.xml",
- "build/.NETFramework/v4.7.2/RedistList/FrameworkList.xml",
- "build/.NETFramework/v4.7.2/System.Activities.Core.Presentation.dll",
- "build/.NETFramework/v4.7.2/System.Activities.Core.Presentation.xml",
- "build/.NETFramework/v4.7.2/System.Activities.DurableInstancing.dll",
- "build/.NETFramework/v4.7.2/System.Activities.DurableInstancing.xml",
- "build/.NETFramework/v4.7.2/System.Activities.Presentation.dll",
- "build/.NETFramework/v4.7.2/System.Activities.Presentation.xml",
- "build/.NETFramework/v4.7.2/System.Activities.dll",
- "build/.NETFramework/v4.7.2/System.Activities.xml",
- "build/.NETFramework/v4.7.2/System.AddIn.Contract.dll",
- "build/.NETFramework/v4.7.2/System.AddIn.Contract.xml",
- "build/.NETFramework/v4.7.2/System.AddIn.dll",
- "build/.NETFramework/v4.7.2/System.AddIn.xml",
- "build/.NETFramework/v4.7.2/System.ComponentModel.Composition.Registration.dll",
- "build/.NETFramework/v4.7.2/System.ComponentModel.Composition.Registration.xml",
- "build/.NETFramework/v4.7.2/System.ComponentModel.Composition.dll",
- "build/.NETFramework/v4.7.2/System.ComponentModel.Composition.xml",
- "build/.NETFramework/v4.7.2/System.ComponentModel.DataAnnotations.dll",
- "build/.NETFramework/v4.7.2/System.ComponentModel.DataAnnotations.xml",
- "build/.NETFramework/v4.7.2/System.Configuration.Install.dll",
- "build/.NETFramework/v4.7.2/System.Configuration.Install.xml",
- "build/.NETFramework/v4.7.2/System.Configuration.dll",
- "build/.NETFramework/v4.7.2/System.Configuration.xml",
- "build/.NETFramework/v4.7.2/System.Core.dll",
- "build/.NETFramework/v4.7.2/System.Core.xml",
- "build/.NETFramework/v4.7.2/System.Data.DataSetExtensions.dll",
- "build/.NETFramework/v4.7.2/System.Data.DataSetExtensions.xml",
- "build/.NETFramework/v4.7.2/System.Data.Entity.Design.dll",
- "build/.NETFramework/v4.7.2/System.Data.Entity.Design.xml",
- "build/.NETFramework/v4.7.2/System.Data.Entity.dll",
- "build/.NETFramework/v4.7.2/System.Data.Entity.xml",
- "build/.NETFramework/v4.7.2/System.Data.Linq.dll",
- "build/.NETFramework/v4.7.2/System.Data.Linq.xml",
- "build/.NETFramework/v4.7.2/System.Data.OracleClient.dll",
- "build/.NETFramework/v4.7.2/System.Data.OracleClient.xml",
- "build/.NETFramework/v4.7.2/System.Data.Services.Client.dll",
- "build/.NETFramework/v4.7.2/System.Data.Services.Client.xml",
- "build/.NETFramework/v4.7.2/System.Data.Services.Design.dll",
- "build/.NETFramework/v4.7.2/System.Data.Services.Design.xml",
- "build/.NETFramework/v4.7.2/System.Data.Services.dll",
- "build/.NETFramework/v4.7.2/System.Data.Services.xml",
- "build/.NETFramework/v4.7.2/System.Data.SqlXml.dll",
- "build/.NETFramework/v4.7.2/System.Data.SqlXml.xml",
- "build/.NETFramework/v4.7.2/System.Data.dll",
- "build/.NETFramework/v4.7.2/System.Data.xml",
- "build/.NETFramework/v4.7.2/System.Deployment.dll",
- "build/.NETFramework/v4.7.2/System.Deployment.xml",
- "build/.NETFramework/v4.7.2/System.Design.dll",
- "build/.NETFramework/v4.7.2/System.Design.xml",
- "build/.NETFramework/v4.7.2/System.Device.dll",
- "build/.NETFramework/v4.7.2/System.Device.xml",
- "build/.NETFramework/v4.7.2/System.Diagnostics.Tracing.dll",
- "build/.NETFramework/v4.7.2/System.Diagnostics.Tracing.xml",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.AccountManagement.dll",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.AccountManagement.xml",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.Protocols.dll",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.Protocols.xml",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.dll",
- "build/.NETFramework/v4.7.2/System.DirectoryServices.xml",
- "build/.NETFramework/v4.7.2/System.Drawing.Design.dll",
- "build/.NETFramework/v4.7.2/System.Drawing.Design.xml",
- "build/.NETFramework/v4.7.2/System.Drawing.dll",
- "build/.NETFramework/v4.7.2/System.Drawing.xml",
- "build/.NETFramework/v4.7.2/System.Dynamic.dll",
- "build/.NETFramework/v4.7.2/System.EnterpriseServices.Thunk.dll",
- "build/.NETFramework/v4.7.2/System.EnterpriseServices.Wrapper.dll",
- "build/.NETFramework/v4.7.2/System.EnterpriseServices.dll",
- "build/.NETFramework/v4.7.2/System.EnterpriseServices.xml",
- "build/.NETFramework/v4.7.2/System.IO.Compression.FileSystem.dll",
- "build/.NETFramework/v4.7.2/System.IO.Compression.FileSystem.xml",
- "build/.NETFramework/v4.7.2/System.IO.Compression.dll",
- "build/.NETFramework/v4.7.2/System.IO.Compression.xml",
- "build/.NETFramework/v4.7.2/System.IO.Log.dll",
- "build/.NETFramework/v4.7.2/System.IO.Log.xml",
- "build/.NETFramework/v4.7.2/System.IdentityModel.Selectors.dll",
- "build/.NETFramework/v4.7.2/System.IdentityModel.Selectors.xml",
- "build/.NETFramework/v4.7.2/System.IdentityModel.Services.dll",
- "build/.NETFramework/v4.7.2/System.IdentityModel.Services.xml",
- "build/.NETFramework/v4.7.2/System.IdentityModel.dll",
- "build/.NETFramework/v4.7.2/System.IdentityModel.xml",
- "build/.NETFramework/v4.7.2/System.Linq.xml",
- "build/.NETFramework/v4.7.2/System.Management.Instrumentation.dll",
- "build/.NETFramework/v4.7.2/System.Management.Instrumentation.xml",
- "build/.NETFramework/v4.7.2/System.Management.dll",
- "build/.NETFramework/v4.7.2/System.Management.xml",
- "build/.NETFramework/v4.7.2/System.Messaging.dll",
- "build/.NETFramework/v4.7.2/System.Messaging.xml",
- "build/.NETFramework/v4.7.2/System.Net.Http.WebRequest.dll",
- "build/.NETFramework/v4.7.2/System.Net.Http.WebRequest.xml",
- "build/.NETFramework/v4.7.2/System.Net.Http.dll",
- "build/.NETFramework/v4.7.2/System.Net.Http.xml",
- "build/.NETFramework/v4.7.2/System.Net.dll",
- "build/.NETFramework/v4.7.2/System.Net.xml",
- "build/.NETFramework/v4.7.2/System.Numerics.dll",
- "build/.NETFramework/v4.7.2/System.Numerics.xml",
- "build/.NETFramework/v4.7.2/System.Printing.dll",
- "build/.NETFramework/v4.7.2/System.Printing.xml",
- "build/.NETFramework/v4.7.2/System.Reflection.Context.dll",
- "build/.NETFramework/v4.7.2/System.Reflection.Context.xml",
- "build/.NETFramework/v4.7.2/System.Runtime.Caching.dll",
- "build/.NETFramework/v4.7.2/System.Runtime.Caching.xml",
- "build/.NETFramework/v4.7.2/System.Runtime.DurableInstancing.dll",
- "build/.NETFramework/v4.7.2/System.Runtime.DurableInstancing.xml",
- "build/.NETFramework/v4.7.2/System.Runtime.Remoting.dll",
- "build/.NETFramework/v4.7.2/System.Runtime.Remoting.xml",
- "build/.NETFramework/v4.7.2/System.Runtime.Serialization.Formatters.Soap.dll",
- "build/.NETFramework/v4.7.2/System.Runtime.Serialization.Formatters.Soap.xml",
- "build/.NETFramework/v4.7.2/System.Runtime.Serialization.dll",
- "build/.NETFramework/v4.7.2/System.Runtime.Serialization.xml",
- "build/.NETFramework/v4.7.2/System.Security.dll",
- "build/.NETFramework/v4.7.2/System.Security.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Activation.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Activation.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Activities.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Activities.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Channels.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Channels.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Discovery.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Discovery.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Routing.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Routing.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Web.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.Web.xml",
- "build/.NETFramework/v4.7.2/System.ServiceModel.dll",
- "build/.NETFramework/v4.7.2/System.ServiceModel.xml",
- "build/.NETFramework/v4.7.2/System.ServiceProcess.dll",
- "build/.NETFramework/v4.7.2/System.ServiceProcess.xml",
- "build/.NETFramework/v4.7.2/System.Speech.dll",
- "build/.NETFramework/v4.7.2/System.Speech.xml",
- "build/.NETFramework/v4.7.2/System.Threading.Tasks.Dataflow.xml",
- "build/.NETFramework/v4.7.2/System.Transactions.dll",
- "build/.NETFramework/v4.7.2/System.Transactions.xml",
- "build/.NETFramework/v4.7.2/System.Web.Abstractions.dll",
- "build/.NETFramework/v4.7.2/System.Web.ApplicationServices.dll",
- "build/.NETFramework/v4.7.2/System.Web.ApplicationServices.xml",
- "build/.NETFramework/v4.7.2/System.Web.DataVisualization.Design.dll",
- "build/.NETFramework/v4.7.2/System.Web.DataVisualization.dll",
- "build/.NETFramework/v4.7.2/System.Web.DataVisualization.xml",
- "build/.NETFramework/v4.7.2/System.Web.DynamicData.Design.dll",
- "build/.NETFramework/v4.7.2/System.Web.DynamicData.Design.xml",
- "build/.NETFramework/v4.7.2/System.Web.DynamicData.dll",
- "build/.NETFramework/v4.7.2/System.Web.DynamicData.xml",
- "build/.NETFramework/v4.7.2/System.Web.Entity.Design.dll",
- "build/.NETFramework/v4.7.2/System.Web.Entity.Design.xml",
- "build/.NETFramework/v4.7.2/System.Web.Entity.dll",
- "build/.NETFramework/v4.7.2/System.Web.Entity.xml",
- "build/.NETFramework/v4.7.2/System.Web.Extensions.Design.dll",
- "build/.NETFramework/v4.7.2/System.Web.Extensions.Design.xml",
- "build/.NETFramework/v4.7.2/System.Web.Extensions.dll",
- "build/.NETFramework/v4.7.2/System.Web.Extensions.xml",
- "build/.NETFramework/v4.7.2/System.Web.Mobile.dll",
- "build/.NETFramework/v4.7.2/System.Web.Mobile.xml",
- "build/.NETFramework/v4.7.2/System.Web.RegularExpressions.dll",
- "build/.NETFramework/v4.7.2/System.Web.RegularExpressions.xml",
- "build/.NETFramework/v4.7.2/System.Web.Routing.dll",
- "build/.NETFramework/v4.7.2/System.Web.Services.dll",
- "build/.NETFramework/v4.7.2/System.Web.Services.xml",
- "build/.NETFramework/v4.7.2/System.Web.dll",
- "build/.NETFramework/v4.7.2/System.Web.xml",
- "build/.NETFramework/v4.7.2/System.Windows.Controls.Ribbon.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Controls.Ribbon.xml",
- "build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.Design.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.xml",
- "build/.NETFramework/v4.7.2/System.Windows.Forms.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Forms.xml",
- "build/.NETFramework/v4.7.2/System.Windows.Input.Manipulations.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Input.Manipulations.xml",
- "build/.NETFramework/v4.7.2/System.Windows.Presentation.dll",
- "build/.NETFramework/v4.7.2/System.Windows.Presentation.xml",
- "build/.NETFramework/v4.7.2/System.Windows.dll",
- "build/.NETFramework/v4.7.2/System.Workflow.Activities.dll",
- "build/.NETFramework/v4.7.2/System.Workflow.Activities.xml",
- "build/.NETFramework/v4.7.2/System.Workflow.ComponentModel.dll",
- "build/.NETFramework/v4.7.2/System.Workflow.ComponentModel.xml",
- "build/.NETFramework/v4.7.2/System.Workflow.Runtime.dll",
- "build/.NETFramework/v4.7.2/System.Workflow.Runtime.xml",
- "build/.NETFramework/v4.7.2/System.WorkflowServices.dll",
- "build/.NETFramework/v4.7.2/System.WorkflowServices.xml",
- "build/.NETFramework/v4.7.2/System.Xaml.dll",
- "build/.NETFramework/v4.7.2/System.Xaml.xml",
- "build/.NETFramework/v4.7.2/System.Xml.Linq.dll",
- "build/.NETFramework/v4.7.2/System.Xml.Linq.xml",
- "build/.NETFramework/v4.7.2/System.Xml.Serialization.dll",
- "build/.NETFramework/v4.7.2/System.Xml.dll",
- "build/.NETFramework/v4.7.2/System.Xml.xml",
- "build/.NETFramework/v4.7.2/System.dll",
- "build/.NETFramework/v4.7.2/System.xml",
- "build/.NETFramework/v4.7.2/UIAutomationClient.dll",
- "build/.NETFramework/v4.7.2/UIAutomationClient.xml",
- "build/.NETFramework/v4.7.2/UIAutomationClientsideProviders.dll",
- "build/.NETFramework/v4.7.2/UIAutomationClientsideProviders.xml",
- "build/.NETFramework/v4.7.2/UIAutomationProvider.dll",
- "build/.NETFramework/v4.7.2/UIAutomationProvider.xml",
- "build/.NETFramework/v4.7.2/UIAutomationTypes.dll",
- "build/.NETFramework/v4.7.2/UIAutomationTypes.xml",
- "build/.NETFramework/v4.7.2/WindowsBase.dll",
- "build/.NETFramework/v4.7.2/WindowsBase.xml",
- "build/.NETFramework/v4.7.2/WindowsFormsIntegration.dll",
- "build/.NETFramework/v4.7.2/WindowsFormsIntegration.xml",
- "build/.NETFramework/v4.7.2/XamlBuildTask.dll",
- "build/.NETFramework/v4.7.2/XamlBuildTask.xml",
- "build/.NETFramework/v4.7.2/mscorlib.dll",
- "build/.NETFramework/v4.7.2/mscorlib.xml",
- "build/.NETFramework/v4.7.2/namespaces.xml",
- "build/.NETFramework/v4.7.2/sysglobl.dll",
- "build/.NETFramework/v4.7.2/sysglobl.xml",
- "build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets",
- "microsoft.netframework.referenceassemblies.net472.1.0.3.nupkg.sha512",
- "microsoft.netframework.referenceassemblies.net472.nuspec"
- ]
- }
- },
- "projectFileDependencyGroups": {
- ".NETFramework,Version=v4.7.2": [
- "Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
- ]
- },
- "packageFolders": {
- "C:\\Users\\gruessing\\.nuget\\packages\\": {}
- },
- "project": {
- "version": "1.0.0",
- "restore": {
- "projectUniqueName": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
- "projectName": "DomainJoinTool",
- "projectPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
- "packagesPath": "C:\\Users\\gruessing\\.nuget\\packages\\",
- "outputPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\obj\\",
- "projectStyle": "PackageReference",
- "configFilePaths": [
- "C:\\Users\\gruessing\\AppData\\Roaming\\NuGet\\NuGet.Config"
- ],
- "originalTargetFrameworks": [
- "net472"
- ],
- "sources": {
- "https://api.nuget.org/v3/index.json": {}
- },
- "frameworks": {
- "net472": {
- "targetAlias": "net472",
- "projectReferences": {}
- }
- },
- "warningProperties": {
- "warnAsError": [
- "NU1605"
- ]
- },
- "restoreAuditProperties": {
- "enableAudit": "true",
- "auditLevel": "low",
- "auditMode": "direct"
- }
- },
- "frameworks": {
- "net472": {
- "targetAlias": "net472",
- "dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies": {
- "suppressParent": "All",
- "target": "Package",
- "version": "[1.0.3, )",
- "autoReferenced": true
- }
- },
- "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.421\\RuntimeIdentifierGraph.json"
- }
- },
- "runtimes": {
- "win7-x64": {
- "#import": []
- }
- }
- }
-}
\ No newline at end of file
diff --git a/domain-join-tool/obj/project.nuget.cache b/domain-join-tool/obj/project.nuget.cache
deleted file mode 100644
index 2021471..0000000
--- a/domain-join-tool/obj/project.nuget.cache
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "version": 2,
- "dgSpecHash": "nUWqqbOYUIM=",
- "success": true,
- "projectFilePath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
- "expectedPackageFiles": [
- "C:\\Users\\gruessing\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
- "C:\\Users\\gruessing\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net472\\1.0.3\\microsoft.netframework.referenceassemblies.net472.1.0.3.nupkg.sha512"
- ],
- "logs": []
-}
\ No newline at end of file
diff --git a/files.zip b/files.zip
deleted file mode 100644
index 4634be4..0000000
Binary files a/files.zip and /dev/null differ
diff --git a/nexus-scanner/Makefile b/nexus-scanner/Makefile
deleted file mode 100644
index dc29b24..0000000
--- a/nexus-scanner/Makefile
+++ /dev/null
@@ -1,49 +0,0 @@
-BINARY := nexus-scanner
-CMD := ./cmd/nexus-scanner
-VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
-LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)"
-
-.PHONY: build run test fmt vet clean install
-
-## build: compile for the local OS/arch
-build:
- go build $(LDFLAGS) -o $(BINARY) $(CMD)
-
-## build-linux: cross-compile for Linux amd64 + arm64
-build-linux:
- GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY)-linux-amd64 $(CMD)
- GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(BINARY)-linux-arm64 $(CMD)
-
-## run: build and run with the example config (local testing only)
-run: build
- ./$(BINARY) config.example.yaml
-
-## test: run all tests
-test:
- go test -v -race ./...
-
-## fmt: format all Go source files
-fmt:
- gofmt -w ./...
-
-## vet: run go vet
-vet:
- go vet ./...
-
-## tidy: update go.sum and remove unused dependencies
-tidy:
- go mod tidy
-
-## clean: remove built binaries
-clean:
- rm -f $(BINARY) $(BINARY)-linux-amd64 $(BINARY)-linux-arm64
-
-## install: deploy to a scanner VM (set SCANNER_HOST env var)
-## Usage: SCANNER_HOST=root@192.168.0.x make install
-install: build-linux
- scp $(BINARY)-linux-amd64 $(SCANNER_HOST):/usr/local/bin/$(BINARY)
- ssh $(SCANNER_HOST) "systemctl restart $(BINARY)"
-
-## help: print this help
-help:
- @grep -E '^## ' Makefile | sed 's/## / /'
diff --git a/nexus-scanner/cmd/nexus-scanner/main.go b/nexus-scanner/cmd/nexus-scanner/main.go
deleted file mode 100644
index 238c801..0000000
--- a/nexus-scanner/cmd/nexus-scanner/main.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package main
-
-import (
- "context"
- "log/slog"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
- "github.com/cereda-systems/nexus-scanner/internal/module"
- arpmod "github.com/cereda-systems/nexus-scanner/internal/modules/arp"
- admod "github.com/cereda-systems/nexus-scanner/internal/modules/adsync"
- dnsmod "github.com/cereda-systems/nexus-scanner/internal/modules/dnsreverse"
- vendormod "github.com/cereda-systems/nexus-scanner/internal/modules/macvendor"
- reportermod "github.com/cereda-systems/nexus-scanner/internal/modules/nexusreporter"
- portscanmod "github.com/cereda-systems/nexus-scanner/internal/modules/portscan"
- sitemon "github.com/cereda-systems/nexus-scanner/internal/modules/sitemon"
- snmpmod "github.com/cereda-systems/nexus-scanner/internal/modules/snmpmod"
- sysarpmod "github.com/cereda-systems/nexus-scanner/internal/modules/sysarp"
- "github.com/cereda-systems/nexus-scanner/internal/scheduler"
- "github.com/cereda-systems/nexus-scanner/internal/web"
-)
-
-var version = "dev"
-
-func main() {
- cfgPath := "/etc/nexus-scanner/config.yaml"
- if len(os.Args) > 1 {
- cfgPath = os.Args[1]
- }
-
- cfg, err := config.Load(cfgPath)
- if err != nil {
- slog.Error("failed to load config", "path", cfgPath, "err", err)
- os.Exit(1)
- }
-
- // Log buffer captures entries for the /logs web page.
- logBuf := web.NewLogBuffer(500)
-
- level := slog.LevelInfo
- if cfg.LogLevel == "debug" {
- level = slog.LevelDebug
- }
- inner := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
- handler := web.NewSlogHandler(inner, logBuf)
- slog.SetDefault(slog.New(handler))
-
- slog.Info("nexus-scanner starting", "version", version, "site", cfg.Site)
-
- store, err := db.Open(cfg.DB.Path)
- if err != nil {
- slog.Error("failed to open database", "path", cfg.DB.Path, "err", err)
- os.Exit(1)
- }
- defer store.Close()
-
- registry := module.NewRegistry()
-
- // sys_arp: reads the OS ARP cache — works on Windows, Linux and macOS
- // without raw-socket privileges. Always enabled.
- registry.Register(sysarpmod.New(cfg.Site, store))
- slog.Info("module registered", "module", "sys_arp")
-
- // dns_reverse: enriches known hosts with reverse-DNS hostnames.
- registry.Register(dnsmod.New(cfg.Site, store))
- slog.Info("module registered", "module", "dns_reverse")
-
- // mac_vendor: assigns OUI-based vendor names to known hosts (runs once at startup).
- registry.Register(vendormod.New(cfg.Site, store))
- slog.Info("module registered", "module", "mac_vendor")
-
- // ad_sync: queries Active Directory for all computer objects.
- if cfg.Modules.ADSync.Enabled {
- registry.Register(admod.New(cfg.Site, cfg.Modules.ADSync, store))
- slog.Info("module registered", "module", "ad_sync",
- "server", cfg.Modules.ADSync.Server,
- "search_base", cfg.Modules.ADSync.SearchBase,
- )
- }
-
- // site_monitoring: availability checks (ping, http, tcp) — always enabled,
- // checks are managed via the web UI.
- cfg.Modules.SiteMonitoring.Enabled = true
- registry.Register(sitemon.New(cfg.Site, cfg.Modules.SiteMonitoring, cfg.Alert, *cfg, store))
- slog.Info("module registered", "module", "site_monitoring",
- "interval", cfg.Modules.SiteMonitoring.Interval)
-
- // port_scan: TCP port scanner for all online hosts.
- cfg.Modules.PortScan.Enabled = true
- registry.Register(portscanmod.New(cfg.Site, cfg.Modules.PortScan, store))
- slog.Info("module registered", "module", "port_scan",
- "interval", cfg.Modules.PortScan.Interval)
-
- // snmp: polls SNMP targets for device info (managed via web UI).
- cfg.Modules.SNMP.Enabled = true
- registry.Register(snmpmod.New(cfg.Site, cfg.Modules.SNMP, store))
- slog.Info("module registered", "module", "snmp",
- "interval", cfg.Modules.SNMP.Interval)
-
- // nexus_reporter: pushes changed assets to IT Nexus API.
- if cfg.Nexus.URL != "" && cfg.Nexus.APIKey != "" {
- cfg.Modules.NexusReporter.Enabled = true
- registry.Register(reportermod.New(cfg.Site, cfg.Modules.NexusReporter, *cfg, store))
- slog.Info("module registered", "module", "nexus_reporter")
- }
-
- // arp_discovery: full ARP sweep via raw sockets (Linux only).
- if cfg.Modules.ARPDiscovery.Enabled {
- registry.Register(arpmod.New(cfg.Site, cfg.Modules.ARPDiscovery, store))
- slog.Info("module registered", "module", "arp_discovery",
- "interval", cfg.Modules.ARPDiscovery.Interval,
- "subnets", cfg.Modules.ARPDiscovery.Subnets,
- )
- }
-
- ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
- defer stop()
-
- // Offline-Sweeper: marks hosts as offline when not seen for OfflineAfter duration.
- go func() {
- ticker := time.NewTicker(5 * time.Minute)
- defer ticker.Stop()
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- n, err := store.MarkStaleHostsOffline(cfg.OfflineAfter)
- if err != nil {
- slog.Error("offline sweep", "err", err)
- } else if n > 0 {
- slog.Info("hosts marked offline", "count", n, "threshold", cfg.OfflineAfter)
- }
- }
- }
- }()
-
- sched := scheduler.New(registry)
- go sched.Run(ctx)
-
- srv := web.NewServer(cfgPath, cfg, store, registry, logBuf)
- go func() {
- if err := srv.ListenAndServe(ctx); err != nil {
- slog.Error("web server stopped", "err", err)
- stop()
- }
- }()
-
- slog.Info("nexus-scanner ready", "addr", cfg.Web.Addr)
- <-ctx.Done()
- slog.Info("nexus-scanner shutting down")
-}
diff --git a/nexus-scanner/config.dev.yaml b/nexus-scanner/config.dev.yaml
deleted file mode 100644
index c0fc063..0000000
--- a/nexus-scanner/config.dev.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-site: LUD
-log_level: debug
-setup_complete: true
-db:
- path: C:/temp/nexus-scanner/scanner.db
-web:
- addr: :8090
- token: demo1234
-nexus:
- url: https://it-nexus.cereda-systems.de
- api_key: nsx-aa0193d719c2be765441e1dd24bf7480
-offline_after: 15m0s
-alert:
- smtp:
- enabled: false
- host: ""
- port: 587
- username: ""
- password: ""
- from: ""
- to: ""
- nexus_enabled: true
-modules:
- arp_discovery:
- enabled: false
- interval: 5m0s
- subnets:
- - 192.168.0.0/24
- interface: eth0
- ad_sync:
- enabled: true
- interval: 30m0s
- server: 192.168.0.12
- port: 389
- bind_dn: svc-scanner@winkel.local
- bind_password: 6ApvVf6X9hvHBxYUqQ3y
- search_base: DC=winkel,DC=local
- tls: false
- site_monitoring:
- enabled: true
- interval: 1m0s
- port_scan:
- enabled: true
- interval: 15m0s
- ports: []
- timeout: 500ms
- snmp:
- enabled: true
- interval: 5m0s
- community: public
- nexus_reporter:
- enabled: true
- interval: 10m0s
diff --git a/nexus-scanner/config.example.yaml b/nexus-scanner/config.example.yaml
deleted file mode 100644
index 5ab197d..0000000
--- a/nexus-scanner/config.example.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-# Nexus Scanner — example configuration
-# Copy to /etc/nexus-scanner/config.yaml and adjust.
-
-site: "LUD" # LUD = Lüdenscheid | BAR = Barleben
-log_level: "info" # debug | info | warn | error
-
-db:
- path: "/var/lib/nexus-scanner/scanner.db"
-
-web:
- addr: ":8080"
- token: "change-me-generate-with-openssl-rand-hex-32"
-
-nexus:
- url: "https://it-nexus.cereda-systems.de"
- api_key: "" # will be filled once IT Nexus wires up the scanner endpoint
-
-modules:
- ad_sync:
- enabled: true
- interval: 30m
- server: "192.168.0.1" # IP des Domain Controllers
- port: 389 # 389 = LDAP, 636 = LDAPS
- bind_dn: "svc-scanner@winkel.local"
- bind_password: "" # Passwort des Service-Accounts
- search_base: "DC=winkel,DC=local"
- tls: false
-
- arp_discovery:
- enabled: true
- interval: 5m # how often to scan (Go duration: 30s, 5m, 1h …)
- interface: "eth0" # network interface to send ARP requests on
- subnets:
- - "192.168.0.0/24" # add more subnets as needed
- # - "10.0.0.0/24"
diff --git a/nexus-scanner/config.firstrun.yaml b/nexus-scanner/config.firstrun.yaml
deleted file mode 100644
index 8272d87..0000000
--- a/nexus-scanner/config.firstrun.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Minimale Startkonfiguration — wird nach dem Setup-Wizard überschrieben.
-site: "LUD"
-log_level: "info"
-setup_complete: false
-
-db:
- path: "/var/lib/nexus-scanner/scanner.db"
-
-web:
- addr: ":8080"
- token: "" # wird im Setup-Wizard gesetzt
-
-nexus:
- url: ""
- api_key: ""
-
-modules:
- arp_discovery:
- enabled: false # wird im Setup-Wizard aktiviert
- interval: 5m
- interface: "eth0"
- subnets: []
diff --git a/nexus-scanner/deploy/config.bar.yaml b/nexus-scanner/deploy/config.bar.yaml
deleted file mode 100644
index b0b7566..0000000
--- a/nexus-scanner/deploy/config.bar.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-site: "BAR"
-log_level: "info"
-setup_complete: false
-
-db:
- path: "/var/lib/nexus-scanner/scanner.db"
-
-web:
- addr: ":8090"
- token: "" # wird beim Setup-Wizard gesetzt
-
-nexus:
- url: "https://it-nexus.cereda-systems.de"
- api_key: "nsx-aa0193d719c2be765441e1dd24bf7480"
-
-offline_after: 15m
-
-alert:
- nexus_enabled: true
- smtp:
- enabled: false
- host: ""
- port: 587
- username: ""
- password: ""
- from: ""
- to: ""
-
-modules:
- arp_discovery:
- enabled: true
- interval: 5m
- subnets:
- - "192.168.10.0/24" # Barleben-Netz anpassen!
- interface: "eth0"
-
- ad_sync:
- enabled: false # DC-Daten für BAR eintragen und auf true setzen
- interval: 30m
- server: ""
- port: 389
- bind_dn: "svc-scanner@winkel.local"
- bind_password: ""
- search_base: "DC=winkel,DC=local"
- tls: false
-
- site_monitoring:
- enabled: true
- interval: 1m
-
- port_scan:
- enabled: true
- interval: 15m
-
- snmp:
- enabled: true
- interval: 5m
- community: "public"
-
- nexus_reporter:
- enabled: true
- interval: 10m
diff --git a/nexus-scanner/deploy/config.lud.yaml b/nexus-scanner/deploy/config.lud.yaml
deleted file mode 100644
index b70b699..0000000
--- a/nexus-scanner/deploy/config.lud.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-site: "LUD"
-log_level: "info"
-setup_complete: false
-
-db:
- path: "/var/lib/nexus-scanner/scanner.db"
-
-web:
- addr: ":8090"
- token: "" # wird beim Setup-Wizard gesetzt
-
-nexus:
- url: "https://it-nexus.cereda-systems.de"
- api_key: "nsx-aa0193d719c2be765441e1dd24bf7480"
-
-offline_after: 15m
-
-alert:
- nexus_enabled: true
- smtp:
- enabled: false
- host: ""
- port: 587
- username: ""
- password: ""
- from: ""
- to: ""
-
-modules:
- arp_discovery:
- enabled: true
- interval: 5m
- subnets:
- - "192.168.0.0/24"
- interface: "eth0"
-
- ad_sync:
- enabled: true
- interval: 30m
- server: "192.168.0.12"
- port: 389
- bind_dn: "svc-scanner@winkel.local"
- bind_password: "6ApvVf6X9hvHBxYUqQ3y"
- search_base: "DC=winkel,DC=local"
- tls: false
-
- site_monitoring:
- enabled: true
- interval: 1m
-
- port_scan:
- enabled: true
- interval: 15m
-
- snmp:
- enabled: true
- interval: 5m
- community: "public"
-
- nexus_reporter:
- enabled: true
- interval: 10m
diff --git a/nexus-scanner/deploy/install.sh b/nexus-scanner/deploy/install.sh
deleted file mode 100644
index 0b327b7..0000000
--- a/nexus-scanner/deploy/install.sh
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/bin/bash
-# Nexus Scanner — Installations-Skript
-# Läuft auf Debian 12 / Ubuntu 22.04+
-# Aufruf: sudo bash install.sh [LUD|BAR]
-set -e
-
-SITE="${1:-LUD}"
-BINARY_URL="https://it-nexus.cereda-systems.de/downloads/nexus-scanner-linux-amd64"
-BINARY="/usr/local/bin/nexus-scanner"
-CONFIG_DIR="/etc/nexus-scanner"
-DATA_DIR="/var/lib/nexus-scanner"
-SERVICE_FILE="/etc/systemd/system/nexus-scanner.service"
-USER="nexus-scanner"
-
-echo "=============================="
-echo " Nexus Scanner Installation"
-echo " Standort: $SITE"
-echo "=============================="
-
-# Abhängigkeiten
-apt-get update -qq
-apt-get install -y -qq curl iputils-ping nmap snmp 2>/dev/null || true
-
-# Benutzer anlegen
-if ! id "$USER" &>/dev/null; then
- useradd --system --no-create-home --shell /usr/sbin/nologin "$USER"
- echo "Benutzer $USER angelegt."
-fi
-
-# Verzeichnisse
-mkdir -p "$CONFIG_DIR" "$DATA_DIR"
-chown "$USER:$USER" "$DATA_DIR"
-
-# Binary installieren (aus deploy/-Verzeichnis oder per Download)
-if [ -f "./nexus-scanner-linux-amd64" ]; then
- cp ./nexus-scanner-linux-amd64 "$BINARY"
- echo "Binary aus lokalem Verzeichnis installiert."
-else
- echo "Binary wird heruntergeladen..."
- curl -fsSL "$BINARY_URL" -o "$BINARY"
-fi
-chmod 755 "$BINARY"
-
-# Config installieren
-if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
- if [ -f "./config.${SITE,,}.yaml" ]; then
- cp "./config.${SITE,,}.yaml" "$CONFIG_DIR/config.yaml"
- else
- # Minimal-Config erzeugen
- cat > "$CONFIG_DIR/config.yaml" << EOF
-site: "$SITE"
-log_level: "info"
-setup_complete: false
-db:
- path: "$DATA_DIR/scanner.db"
-web:
- addr: ":8090"
- token: ""
-nexus:
- url: "https://it-nexus.cereda-systems.de"
- api_key: "nsx-aa0193d719c2be765441e1dd24bf7480"
-offline_after: 15m
-modules:
- arp_discovery:
- enabled: true
- interval: 5m
- subnets: ["192.168.0.0/24"]
- interface: "eth0"
- site_monitoring:
- enabled: true
- interval: 1m
- port_scan:
- enabled: true
- interval: 15m
- snmp:
- enabled: true
- interval: 5m
- community: "public"
- nexus_reporter:
- enabled: true
- interval: 10m
-EOF
- fi
- echo "Config nach $CONFIG_DIR/config.yaml installiert."
-fi
-chown "$USER":"$USER" "$CONFIG_DIR/config.yaml"
-chmod 640 "$CONFIG_DIR/config.yaml"
-
-# systemd Service installieren
-cat > "$SERVICE_FILE" << 'EOF'
-[Unit]
-Description=Nexus Scanner — Netzwerk-Discovery Agent
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-Type=simple
-User=nexus-scanner
-Group=nexus-scanner
-ExecStart=/usr/local/bin/nexus-scanner /etc/nexus-scanner/config.yaml
-Restart=on-failure
-RestartSec=10
-StandardOutput=journal
-StandardError=journal
-SyslogIdentifier=nexus-scanner
-NoNewPrivileges=yes
-ProtectSystem=strict
-ProtectHome=yes
-ReadWritePaths=/var/lib/nexus-scanner /etc/nexus-scanner
-PrivateTmp=yes
-AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-systemctl daemon-reload
-systemctl enable nexus-scanner
-systemctl restart nexus-scanner
-
-IP=$(hostname -I | awk '{print $1}')
-echo ""
-echo "=============================="
-echo " Installation abgeschlossen!"
-echo "=============================="
-echo ""
-echo " Web-UI: http://$IP:8090"
-echo " Status: systemctl status nexus-scanner"
-echo " Logs: journalctl -u nexus-scanner -f"
-echo ""
-echo " Öffne http://$IP:8090 im Browser"
-echo " um den Setup-Wizard abzuschließen."
-echo ""
diff --git a/nexus-scanner/deploy/nexus-scanner.service b/nexus-scanner/deploy/nexus-scanner.service
deleted file mode 100644
index c2dc2f5..0000000
--- a/nexus-scanner/deploy/nexus-scanner.service
+++ /dev/null
@@ -1,27 +0,0 @@
-[Unit]
-Description=Nexus Scanner — Netzwerk-Discovery Agent
-Documentation=https://it-nexus.cereda-systems.de
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-Type=simple
-User=nexus-scanner
-Group=nexus-scanner
-ExecStart=/usr/local/bin/nexus-scanner /etc/nexus-scanner/config.yaml
-Restart=on-failure
-RestartSec=10
-StandardOutput=journal
-StandardError=journal
-SyslogIdentifier=nexus-scanner
-
-# Security hardening
-NoNewPrivileges=yes
-ProtectSystem=strict
-ProtectHome=yes
-ReadWritePaths=/var/lib/nexus-scanner
-PrivateTmp=yes
-CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN
-
-[Install]
-WantedBy=multi-user.target
diff --git a/nexus-scanner/go.mod b/nexus-scanner/go.mod
deleted file mode 100644
index 5174aab..0000000
--- a/nexus-scanner/go.mod
+++ /dev/null
@@ -1,36 +0,0 @@
-module github.com/cereda-systems/nexus-scanner
-
-go 1.24.0
-
-require (
- github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875
- gopkg.in/yaml.v3 v3.0.1
- modernc.org/sqlite v1.29.10
-)
-
-require (
- github.com/Azure/go-ntlmssp v0.1.0 // indirect
- github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
- github.com/go-ldap/ldap/v3 v3.4.13 // indirect
- github.com/google/uuid v1.6.0 // indirect
- github.com/gosnmp/gosnmp v1.43.2 // indirect
- github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
- github.com/josharian/native v1.0.0 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
- github.com/mdlayher/packet v1.0.0 // indirect
- github.com/mdlayher/socket v0.2.1 // indirect
- github.com/ncruces/go-strftime v0.1.9 // indirect
- github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
- golang.org/x/crypto v0.48.0 // indirect
- golang.org/x/net v0.50.0 // indirect
- golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
- golang.org/x/sys v0.41.0 // indirect
- modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
- modernc.org/libc v1.49.3 // indirect
- modernc.org/mathutil v1.6.0 // indirect
- modernc.org/memory v1.8.0 // indirect
- modernc.org/strutil v1.2.0 // indirect
- modernc.org/token v1.1.0 // indirect
-)
diff --git a/nexus-scanner/go.sum b/nexus-scanner/go.sum
deleted file mode 100644
index 4aa3536..0000000
--- a/nexus-scanner/go.sum
+++ /dev/null
@@ -1,92 +0,0 @@
-github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
-github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
-github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
-github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
-github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
-github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
-github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gosnmp/gosnmp v1.43.2 h1:F9loz6uMCNtIQj0RNO5wz/mZ+FZt2WyNKJYOvw+Zosw=
-github.com/gosnmp/gosnmp v1.43.2/go.mod h1:smHIwoaqr1M+HTAEd7+mKkPs8lp3Lf/U+htPUql1Q3c=
-github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
-github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
-github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
-github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 h1:ql8x//rJsHMjS+qqEag8n3i4azw1QneKh5PieH9UEbY=
-github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875/go.mod h1:kfOoFJuHWp76v1RgZCb9/gVUc7XdY877S2uVYbNliGc=
-github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE=
-github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og=
-github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8=
-github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=
-github.com/mdlayher/socket v0.2.1 h1:F2aaOwb53VsBE+ebRS9bLd7yPOfYUMC8lOODdCBDY6w=
-github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
-github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
-github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
-golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
-golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
-modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
-modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
-modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
-modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
-modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
-modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
-modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
-modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
-modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
-modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
-modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
-modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
-modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
-modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
-modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
-modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/nexus-scanner/internal/config/config.go b/nexus-scanner/internal/config/config.go
deleted file mode 100644
index 8c3af61..0000000
--- a/nexus-scanner/internal/config/config.go
+++ /dev/null
@@ -1,195 +0,0 @@
-package config
-
-import (
- "fmt"
- "os"
- "time"
-
- "gopkg.in/yaml.v3"
-)
-
-// Config holds the full agent configuration loaded from config.yaml.
-type Config struct {
- Site string `yaml:"site"` // LUD or BAR
- LogLevel string `yaml:"log_level"` // debug | info | warn | error
- SetupComplete bool `yaml:"setup_complete"` // false → show setup wizard on first run
-
- DB struct {
- Path string `yaml:"path"`
- } `yaml:"db"`
-
- Web struct {
- Addr string `yaml:"addr"`
- Token string `yaml:"token"`
- } `yaml:"web"`
-
- Nexus struct {
- URL string `yaml:"url"`
- APIKey string `yaml:"api_key"`
- } `yaml:"nexus"`
-
- OfflineAfter time.Duration `yaml:"offline_after"` // mark hosts offline after this duration
-
- Alert AlertConfig `yaml:"alert"`
-
- Modules struct {
- ARPDiscovery ARPConfig `yaml:"arp_discovery"`
- ADSync ADConfig `yaml:"ad_sync"`
- SiteMonitoring SiteMonConfig `yaml:"site_monitoring"`
- PortScan PortScanConfig `yaml:"port_scan"`
- SNMP SNMPConfig `yaml:"snmp"`
- NexusReporter ReporterConfig `yaml:"nexus_reporter"`
- } `yaml:"modules"`
-}
-
-// ADConfig holds settings for the ad_sync module.
-type ADConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
- Server string `yaml:"server"` // DC hostname or IP
- Port int `yaml:"port"` // default 389
- BindDN string `yaml:"bind_dn"` // svc-scanner@winkel.local
- BindPassword string `yaml:"bind_password"`
- SearchBase string `yaml:"search_base"` // DC=winkel,DC=local
- TLS bool `yaml:"tls"` // use LDAPS
-}
-
-// SiteMonConfig holds settings for the site_monitoring module.
-type SiteMonConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
-}
-
-// PortScanConfig holds settings for the port_scan module.
-type PortScanConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
- Ports []int `yaml:"ports"` // empty = use defaults
- Timeout time.Duration `yaml:"timeout"` // per-port connect timeout
-}
-
-// SNMPConfig holds settings for the snmp module.
-type SNMPConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
- Community string `yaml:"community"` // default community string
-}
-
-// ReporterConfig holds settings for the nexus_reporter module.
-type ReporterConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
-}
-
-// AlertConfig holds notification settings.
-type AlertConfig struct {
- SMTP struct {
- Enabled bool `yaml:"enabled"`
- Host string `yaml:"host"`
- Port int `yaml:"port"`
- Username string `yaml:"username"`
- Password string `yaml:"password"`
- From string `yaml:"from"`
- To string `yaml:"to"`
- } `yaml:"smtp"`
- NexusEnabled bool `yaml:"nexus_enabled"`
-}
-
-// ARPConfig holds settings for the arp_discovery module.
-type ARPConfig struct {
- Enabled bool `yaml:"enabled"`
- Interval time.Duration `yaml:"interval"`
- Subnets []string `yaml:"subnets"`
- Interface string `yaml:"interface"`
-}
-
-// Load reads and validates the YAML config at path.
-func Load(path string) (*Config, error) {
- f, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("open config %q: %w", path, err)
- }
- defer f.Close()
-
- var cfg Config
- if err := yaml.NewDecoder(f).Decode(&cfg); err != nil {
- return nil, fmt.Errorf("decode config: %w", err)
- }
-
- if err := cfg.applyDefaults(); err != nil {
- return nil, fmt.Errorf("config validation: %w", err)
- }
-
- return &cfg, nil
-}
-
-// Save writes the config back to path in YAML format.
-func Save(path string, cfg *Config) error {
- f, err := os.Create(path)
- if err != nil {
- return fmt.Errorf("create config %q: %w", path, err)
- }
- defer f.Close()
- if err := yaml.NewEncoder(f).Encode(cfg); err != nil {
- return fmt.Errorf("encode config: %w", err)
- }
- return nil
-}
-
-func (c *Config) applyDefaults() error {
- if c.Site == "" {
- return fmt.Errorf("site is required (LUD or BAR)")
- }
- if c.Site != "LUD" && c.Site != "BAR" {
- return fmt.Errorf("site must be LUD or BAR, got %q", c.Site)
- }
- if c.LogLevel == "" {
- c.LogLevel = "info"
- }
- if c.DB.Path == "" {
- c.DB.Path = "/var/lib/nexus-scanner/scanner.db"
- }
- if c.Web.Addr == "" {
- c.Web.Addr = ":8080"
- }
- if c.Modules.ARPDiscovery.Interval == 0 {
- c.Modules.ARPDiscovery.Interval = 5 * time.Minute
- }
- if c.Modules.ARPDiscovery.Interface == "" {
- c.Modules.ARPDiscovery.Interface = "eth0"
- }
- if c.Modules.ADSync.Port == 0 {
- c.Modules.ADSync.Port = 389
- }
- if c.Modules.ADSync.Interval == 0 {
- c.Modules.ADSync.Interval = 30 * time.Minute
- }
- if c.Modules.ADSync.SearchBase == "" && c.Modules.ADSync.Server != "" {
- c.Modules.ADSync.SearchBase = "DC=winkel,DC=local"
- }
- if c.Modules.SiteMonitoring.Interval == 0 {
- c.Modules.SiteMonitoring.Interval = time.Minute
- }
- if c.Modules.PortScan.Interval == 0 {
- c.Modules.PortScan.Interval = 15 * time.Minute
- }
- if c.Modules.PortScan.Timeout == 0 {
- c.Modules.PortScan.Timeout = 500 * time.Millisecond
- }
- if c.Modules.SNMP.Interval == 0 {
- c.Modules.SNMP.Interval = 5 * time.Minute
- }
- if c.Modules.SNMP.Community == "" {
- c.Modules.SNMP.Community = "public"
- }
- if c.Modules.NexusReporter.Interval == 0 {
- c.Modules.NexusReporter.Interval = 10 * time.Minute
- }
- if c.OfflineAfter == 0 {
- c.OfflineAfter = 15 * time.Minute
- }
- if c.Alert.SMTP.Port == 0 {
- c.Alert.SMTP.Port = 587
- }
- return nil
-}
diff --git a/nexus-scanner/internal/db/db.go b/nexus-scanner/internal/db/db.go
deleted file mode 100644
index 3aba8dc..0000000
--- a/nexus-scanner/internal/db/db.go
+++ /dev/null
@@ -1,659 +0,0 @@
-package db
-
-import (
- "database/sql"
- "fmt"
- "time"
-
- _ "modernc.org/sqlite"
-)
-
-// Store wraps the SQLite database.
-type Store struct {
- db *sql.DB
-}
-
-// Open opens (or creates) the SQLite database at path and runs migrations.
-func Open(path string) (*Store, error) {
- conn, err := sql.Open("sqlite", path)
- if err != nil {
- return nil, fmt.Errorf("open sqlite at %q: %w", path, err)
- }
-
- // SQLite works best with a single writer connection.
- conn.SetMaxOpenConns(1)
-
- // Recommended pragmas for reliability and performance.
- for _, pragma := range []string{
- "PRAGMA journal_mode=WAL",
- "PRAGMA busy_timeout=5000",
- "PRAGMA foreign_keys=ON",
- } {
- if _, err := conn.Exec(pragma); err != nil {
- conn.Close()
- return nil, fmt.Errorf("pragma %q: %w", pragma, err)
- }
- }
-
- s := &Store{db: conn}
- if err := s.migrate(); err != nil {
- conn.Close()
- return nil, fmt.Errorf("migrate: %w", err)
- }
-
- return s, nil
-}
-
-// Close closes the underlying database connection.
-func (s *Store) Close() error {
- return s.db.Close()
-}
-
-func (s *Store) migrate() error {
- _, err := s.db.Exec(`
- CREATE TABLE IF NOT EXISTS host_ports (
- ip TEXT NOT NULL,
- port INTEGER NOT NULL,
- scanned_at DATETIME NOT NULL,
- PRIMARY KEY (ip, port)
- );
-
- CREATE TABLE IF NOT EXISTS snmp_targets (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- ip TEXT NOT NULL,
- community TEXT NOT NULL DEFAULT 'public',
- version TEXT NOT NULL DEFAULT 'v2c',
- type TEXT NOT NULL DEFAULT 'generic',
- enabled INTEGER NOT NULL DEFAULT 1,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
-
- CREATE TABLE IF NOT EXISTS snmp_results (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- target_id INTEGER NOT NULL,
- oid_name TEXT NOT NULL,
- value TEXT NOT NULL DEFAULT '',
- scanned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE(target_id, oid_name)
- );
-
- CREATE TABLE IF NOT EXISTS module_states (
- name TEXT PRIMARY KEY,
- enabled INTEGER NOT NULL DEFAULT 1
- );
-
- CREATE TABLE IF NOT EXISTS monitor_checks (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- type TEXT NOT NULL,
- target TEXT NOT NULL,
- enabled INTEGER NOT NULL DEFAULT 1,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
-
- CREATE TABLE IF NOT EXISTS monitor_results (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- check_id INTEGER NOT NULL,
- status TEXT NOT NULL,
- latency_ms INTEGER NOT NULL DEFAULT 0,
- error TEXT NOT NULL DEFAULT '',
- checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
-
- CREATE INDEX IF NOT EXISTS idx_monitor_results_check
- ON monitor_results(check_id, checked_at DESC);
-
- CREATE TABLE IF NOT EXISTS ad_computers (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- cn TEXT NOT NULL UNIQUE,
- dn TEXT NOT NULL DEFAULT '',
- os TEXT NOT NULL DEFAULT '',
- os_version TEXT NOT NULL DEFAULT '',
- owner TEXT NOT NULL DEFAULT '',
- department TEXT NOT NULL DEFAULT '',
- last_logon DATETIME,
- ad_created DATETIME,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
-
- CREATE TABLE IF NOT EXISTS hosts (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- ip TEXT NOT NULL UNIQUE,
- mac TEXT NOT NULL,
- vendor TEXT NOT NULL DEFAULT '',
- hostname TEXT NOT NULL DEFAULT '',
- site TEXT NOT NULL,
- first_seen DATETIME NOT NULL,
- last_seen DATETIME NOT NULL,
- status TEXT NOT NULL DEFAULT 'online'
- );
-
- CREATE TABLE IF NOT EXISTS scan_runs (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- module TEXT NOT NULL,
- started_at DATETIME NOT NULL,
- ended_at DATETIME,
- status TEXT NOT NULL DEFAULT 'running',
- error TEXT NOT NULL DEFAULT ''
- );
- `)
- return err
-}
-
-// Host represents a discovered network device.
-type Host struct {
- ID int64
- IP string
- MAC string
- Vendor string
- Hostname string
- Site string
- FirstSeen time.Time
- LastSeen time.Time
- Status string
-}
-
-// UpsertHost inserts a new host or updates MAC, vendor and last_seen if the
-// IP already exists. first_seen is never overwritten on update.
-func (s *Store) UpsertHost(h Host) error {
- _, err := s.db.Exec(`
- INSERT INTO hosts (ip, mac, vendor, hostname, site, first_seen, last_seen, status)
- VALUES (?, ?, ?, ?, ?, ?, ?, 'online')
- ON CONFLICT(ip) DO UPDATE SET
- mac = excluded.mac,
- vendor = CASE WHEN excluded.vendor != '' THEN excluded.vendor ELSE vendor END,
- hostname = CASE WHEN excluded.hostname != '' THEN excluded.hostname ELSE hostname END,
- last_seen = excluded.last_seen,
- status = 'online'
- `, h.IP, h.MAC, h.Vendor, h.Hostname, h.Site, h.FirstSeen.UTC(), h.LastSeen.UTC())
- if err != nil {
- return fmt.Errorf("upsert host %s: %w", h.IP, err)
- }
- return nil
-}
-
-// ListHosts returns all hosts ordered by last_seen desc.
-// Pass an empty site to return all sites.
-func (s *Store) ListHosts(site string) ([]Host, error) {
- query := `
- SELECT id, ip, mac, vendor, hostname, site, first_seen, last_seen, status
- FROM hosts`
- var args []any
- if site != "" {
- query += ` WHERE site = ?`
- args = append(args, site)
- }
- query += ` ORDER BY last_seen DESC`
-
- rows, err := s.db.Query(query, args...)
- if err != nil {
- return nil, fmt.Errorf("list hosts: %w", err)
- }
- defer rows.Close()
-
- var hosts []Host
- for rows.Next() {
- var h Host
- if err := rows.Scan(
- &h.ID, &h.IP, &h.MAC, &h.Vendor, &h.Hostname,
- &h.Site, &h.FirstSeen, &h.LastSeen, &h.Status,
- ); err != nil {
- return nil, fmt.Errorf("scan host row: %w", err)
- }
- hosts = append(hosts, h)
- }
- return hosts, rows.Err()
-}
-
-// CountHosts returns the total number of hosts in the database.
-func (s *Store) CountHosts() (int, error) {
- var n int
- if err := s.db.QueryRow(`SELECT COUNT(*) FROM hosts`).Scan(&n); err != nil {
- return 0, fmt.Errorf("count hosts: %w", err)
- }
- return n, nil
-}
-
-// BeginScan records the start of a module scan run and returns the run ID.
-func (s *Store) BeginScan(module string) (int64, error) {
- res, err := s.db.Exec(`
- INSERT INTO scan_runs (module, started_at) VALUES (?, ?)
- `, module, time.Now().UTC())
- if err != nil {
- return 0, fmt.Errorf("begin scan: %w", err)
- }
- return res.LastInsertId()
-}
-
-// EndScan marks a scan run as completed (status ok or error).
-func (s *Store) EndScan(id int64, scanErr error) error {
- status, errMsg := "ok", ""
- if scanErr != nil {
- status, errMsg = "error", scanErr.Error()
- }
- _, err := s.db.Exec(`
- UPDATE scan_runs SET ended_at = ?, status = ?, error = ? WHERE id = ?
- `, time.Now().UTC(), status, errMsg, id)
- if err != nil {
- return fmt.Errorf("end scan %d: %w", id, err)
- }
- return nil
-}
-
-// ADComputer represents a computer object from Active Directory.
-type ADComputer struct {
- ID int64
- CN string
- DN string
- OS string
- OSVersion string
- Owner string
- Department string
- LastLogon time.Time
- ADCreated time.Time
- UpdatedAt time.Time
-}
-
-// UpsertADComputer inserts or updates an AD computer record by CN.
-func (s *Store) UpsertADComputer(c ADComputer) error {
- _, err := s.db.Exec(`
- INSERT INTO ad_computers (cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(cn) DO UPDATE SET
- dn = excluded.dn,
- os = excluded.os,
- os_version = excluded.os_version,
- owner = excluded.owner,
- department = excluded.department,
- last_logon = excluded.last_logon,
- updated_at = excluded.updated_at
- `, c.CN, c.DN, c.OS, c.OSVersion, c.Owner, c.Department,
- nullTime(c.LastLogon), nullTime(c.ADCreated), time.Now().UTC())
- if err != nil {
- return fmt.Errorf("upsert ad computer %s: %w", c.CN, err)
- }
- return nil
-}
-
-// ListADComputers returns all known AD computers.
-func (s *Store) ListADComputers() ([]ADComputer, error) {
- rows, err := s.db.Query(`
- SELECT id, cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at
- FROM ad_computers ORDER BY cn
- `)
- if err != nil {
- return nil, fmt.Errorf("list ad computers: %w", err)
- }
- defer rows.Close()
-
- var out []ADComputer
- for rows.Next() {
- var c ADComputer
- var lastLogon, adCreated *time.Time
- if err := rows.Scan(&c.ID, &c.CN, &c.DN, &c.OS, &c.OSVersion,
- &c.Owner, &c.Department, &lastLogon, &adCreated, &c.UpdatedAt); err != nil {
- return nil, err
- }
- if lastLogon != nil {
- c.LastLogon = *lastLogon
- }
- if adCreated != nil {
- c.ADCreated = *adCreated
- }
- out = append(out, c)
- }
- return out, rows.Err()
-}
-
-// UpdateHostnameFromAD sets the hostname of a host matching the given IP.
-func (s *Store) UpdateHostnameFromAD(hostname string) error {
- _, err := s.db.Exec(`
- UPDATE hosts SET hostname = ? WHERE hostname = '' AND ip IN (
- SELECT ip FROM hosts WHERE LOWER(hostname) = LOWER(?)
- )
- `, hostname, hostname)
- return err
-}
-
-func nullTime(t time.Time) any {
- if t.IsZero() {
- return nil
- }
- return t.UTC()
-}
-
-// LastScanTime returns when the given module last ran successfully.
-func (s *Store) LastScanTime(module string) (time.Time, error) {
- var t time.Time
- err := s.db.QueryRow(`
- SELECT ended_at FROM scan_runs
- WHERE module = ? AND status = 'ok'
- ORDER BY ended_at DESC LIMIT 1
- `, module).Scan(&t)
- if err == sql.ErrNoRows {
- return time.Time{}, nil
- }
- return t, err
-}
-
-/* ── Site Monitoring ──────────────────────────────────────────────── */
-
-// MonitorCheck is a configured availability check (ping/http/tcp).
-type MonitorCheck struct {
- ID int64
- Name string
- Type string // ping | http | tcp
- Target string
- Enabled bool
- CreatedAt time.Time
-}
-
-// MonitorResult is a single check execution result.
-type MonitorResult struct {
- ID int64
- CheckID int64
- Status string // online | offline
- LatencyMS int
- Error string
- CheckedAt time.Time
-}
-
-// ListMonitorChecks returns all configured checks ordered by creation time.
-func (s *Store) ListMonitorChecks() ([]MonitorCheck, error) {
- rows, err := s.db.Query(`
- SELECT id, name, type, target, enabled, created_at
- FROM monitor_checks ORDER BY id
- `)
- if err != nil {
- return nil, fmt.Errorf("list monitor checks: %w", err)
- }
- defer rows.Close()
- var out []MonitorCheck
- for rows.Next() {
- var c MonitorCheck
- var ena int
- if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.Target, &ena, &c.CreatedAt); err != nil {
- return nil, err
- }
- c.Enabled = ena == 1
- out = append(out, c)
- }
- return out, rows.Err()
-}
-
-// AddMonitorCheck inserts a new check definition.
-func (s *Store) AddMonitorCheck(name, typ, target string) (int64, error) {
- res, err := s.db.Exec(`
- INSERT INTO monitor_checks (name, type, target) VALUES (?, ?, ?)
- `, name, typ, target)
- if err != nil {
- return 0, fmt.Errorf("add monitor check: %w", err)
- }
- return res.LastInsertId()
-}
-
-// DeleteMonitorCheck removes a check and all its results.
-func (s *Store) DeleteMonitorCheck(id int64) error {
- if _, err := s.db.Exec(`DELETE FROM monitor_results WHERE check_id = ?`, id); err != nil {
- return fmt.Errorf("delete monitor results: %w", err)
- }
- if _, err := s.db.Exec(`DELETE FROM monitor_checks WHERE id = ?`, id); err != nil {
- return fmt.Errorf("delete monitor check: %w", err)
- }
- return nil
-}
-
-// InsertMonitorResult stores one check execution result.
-func (s *Store) InsertMonitorResult(r MonitorResult) error {
- _, err := s.db.Exec(`
- INSERT INTO monitor_results (check_id, status, latency_ms, error, checked_at)
- VALUES (?, ?, ?, ?, ?)
- `, r.CheckID, r.Status, r.LatencyMS, r.Error, r.CheckedAt.UTC())
- if err != nil {
- return fmt.Errorf("insert monitor result: %w", err)
- }
- return nil
-}
-
-// LatestMonitorResults returns the most recent result for each check_id.
-func (s *Store) LatestMonitorResults() (map[int64]MonitorResult, error) {
- rows, err := s.db.Query(`
- SELECT r.id, r.check_id, r.status, r.latency_ms, r.error, r.checked_at
- FROM monitor_results r
- INNER JOIN (
- SELECT check_id, MAX(id) as max_id FROM monitor_results GROUP BY check_id
- ) latest ON r.id = latest.max_id
- `)
- if err != nil {
- return nil, fmt.Errorf("latest monitor results: %w", err)
- }
- defer rows.Close()
- out := make(map[int64]MonitorResult)
- for rows.Next() {
- var r MonitorResult
- if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
- return nil, err
- }
- out[r.CheckID] = r
- }
- return out, rows.Err()
-}
-
-// MonitorResultHistory returns the last limit results for a given check.
-func (s *Store) MonitorResultHistory(checkID int64, limit int) ([]MonitorResult, error) {
- rows, err := s.db.Query(`
- SELECT id, check_id, status, latency_ms, error, checked_at
- FROM monitor_results WHERE check_id = ?
- ORDER BY checked_at DESC LIMIT ?
- `, checkID, limit)
- if err != nil {
- return nil, fmt.Errorf("monitor history: %w", err)
- }
- defer rows.Close()
- var out []MonitorResult
- for rows.Next() {
- var r MonitorResult
- if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
- return nil, err
- }
- out = append(out, r)
- }
- // Reverse so oldest-first for sparkline rendering.
- for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
- out[i], out[j] = out[j], out[i]
- }
- return out, rows.Err()
-}
-
-/* ── Module States ────────────────────────────────────────────────── */
-
-// SetModuleEnabled stores the enabled state for a module.
-func (s *Store) SetModuleEnabled(name string, enabled bool) error {
- v := 0
- if enabled {
- v = 1
- }
- _, err := s.db.Exec(`
- INSERT INTO module_states (name, enabled) VALUES (?,?)
- ON CONFLICT(name) DO UPDATE SET enabled=excluded.enabled
- `, name, v)
- return err
-}
-
-// GetModuleEnabled returns whether a module is enabled (default true if not set).
-func (s *Store) GetModuleEnabled(name string) bool {
- var v int
- err := s.db.QueryRow(`SELECT enabled FROM module_states WHERE name=?`, name).Scan(&v)
- if err != nil {
- return true // default: enabled
- }
- return v == 1
-}
-
-/* ── Port Scan ────────────────────────────────────────────────────── */
-
-// UpsertHostPorts replaces all open ports for an IP.
-func (s *Store) UpsertHostPorts(ip string, ports []int) error {
- tx, err := s.db.Begin()
- if err != nil {
- return err
- }
- defer tx.Rollback()
- if _, err := tx.Exec(`DELETE FROM host_ports WHERE ip = ?`, ip); err != nil {
- return err
- }
- now := time.Now().UTC()
- for _, p := range ports {
- if _, err := tx.Exec(`INSERT INTO host_ports (ip, port, scanned_at) VALUES (?,?,?)`, ip, p, now); err != nil {
- return err
- }
- }
- return tx.Commit()
-}
-
-// GetHostPorts returns all open ports for an IP.
-func (s *Store) GetHostPorts(ip string) ([]int, error) {
- rows, err := s.db.Query(`SELECT port FROM host_ports WHERE ip = ? ORDER BY port`, ip)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var ports []int
- for rows.Next() {
- var p int
- rows.Scan(&p)
- ports = append(ports, p)
- }
- return ports, rows.Err()
-}
-
-// ListAllHostPorts returns a map of IP → open ports.
-func (s *Store) ListAllHostPorts() (map[string][]int, error) {
- rows, err := s.db.Query(`SELECT ip, port FROM host_ports ORDER BY ip, port`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- out := make(map[string][]int)
- for rows.Next() {
- var ip string
- var port int
- rows.Scan(&ip, &port)
- out[ip] = append(out[ip], port)
- }
- return out, rows.Err()
-}
-
-// MarkStaleHostsOffline marks hosts as offline if last_seen is older than threshold.
-func (s *Store) MarkStaleHostsOffline(olderThan time.Duration) (int, error) {
- threshold := time.Now().UTC().Add(-olderThan)
- res, err := s.db.Exec(`UPDATE hosts SET status='offline' WHERE status='online' AND last_seen < ?`, threshold)
- if err != nil {
- return 0, fmt.Errorf("mark stale offline: %w", err)
- }
- n, _ := res.RowsAffected()
- return int(n), nil
-}
-
-/* ── SNMP ─────────────────────────────────────────────────────────── */
-
-// SNMPTarget is a configured SNMP polling target.
-type SNMPTarget struct {
- ID int64
- Name string
- IP string
- Community string
- Version string
- Type string // generic | printer | switch
- Enabled bool
- CreatedAt time.Time
-}
-
-// SNMPResult is the latest value for one OID on one target.
-type SNMPResult struct {
- ID int64
- TargetID int64
- OIDName string
- Value string
- ScannedAt time.Time
-}
-
-// ListSNMPTargets returns all SNMP targets.
-func (s *Store) ListSNMPTargets() ([]SNMPTarget, error) {
- rows, err := s.db.Query(`SELECT id,name,ip,community,version,type,enabled,created_at FROM snmp_targets ORDER BY id`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var out []SNMPTarget
- for rows.Next() {
- var t SNMPTarget
- var ena int
- rows.Scan(&t.ID, &t.Name, &t.IP, &t.Community, &t.Version, &t.Type, &ena, &t.CreatedAt)
- t.Enabled = ena == 1
- out = append(out, t)
- }
- return out, rows.Err()
-}
-
-// AddSNMPTarget inserts a new SNMP target.
-func (s *Store) AddSNMPTarget(name, ip, community, version, typ string) (int64, error) {
- res, err := s.db.Exec(`INSERT INTO snmp_targets (name,ip,community,version,type) VALUES (?,?,?,?,?)`,
- name, ip, community, version, typ)
- if err != nil {
- return 0, err
- }
- return res.LastInsertId()
-}
-
-// DeleteSNMPTarget removes a target and its results.
-func (s *Store) DeleteSNMPTarget(id int64) error {
- s.db.Exec(`DELETE FROM snmp_results WHERE target_id = ?`, id)
- _, err := s.db.Exec(`DELETE FROM snmp_targets WHERE id = ?`, id)
- return err
-}
-
-// UpsertSNMPResult inserts or updates one OID result.
-func (s *Store) UpsertSNMPResult(r SNMPResult) error {
- _, err := s.db.Exec(`
- INSERT INTO snmp_results (target_id, oid_name, value, scanned_at)
- VALUES (?,?,?,?)
- ON CONFLICT(target_id, oid_name) DO UPDATE SET value=excluded.value, scanned_at=excluded.scanned_at
- `, r.TargetID, r.OIDName, r.Value, r.ScannedAt.UTC())
- return err
-}
-
-// LatestSNMPResults returns all latest OID values keyed by target_id.
-func (s *Store) LatestSNMPResults() (map[int64]map[string]string, error) {
- rows, err := s.db.Query(`SELECT target_id, oid_name, value FROM snmp_results ORDER BY target_id, oid_name`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- out := make(map[int64]map[string]string)
- for rows.Next() {
- var tid int64
- var k, v string
- rows.Scan(&tid, &k, &v)
- if out[tid] == nil {
- out[tid] = make(map[string]string)
- }
- out[tid][k] = v
- }
- return out, rows.Err()
-}
-
-// CleanupMonitorResults deletes old results, keeping the last keepPerCheck per check.
-func (s *Store) CleanupMonitorResults(keepPerCheck int) error {
- _, err := s.db.Exec(`
- DELETE FROM monitor_results WHERE id NOT IN (
- SELECT id FROM (
- SELECT id, ROW_NUMBER() OVER (
- PARTITION BY check_id ORDER BY checked_at DESC
- ) AS rn FROM monitor_results
- ) WHERE rn <= ?
- )
- `, keepPerCheck)
- return err
-}
diff --git a/nexus-scanner/internal/module/module.go b/nexus-scanner/internal/module/module.go
deleted file mode 100644
index 8772445..0000000
--- a/nexus-scanner/internal/module/module.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package module
-
-import (
- "context"
- "fmt"
- "time"
-)
-
-// Module is the interface every scanner module must implement.
-type Module interface {
- // Name returns the unique identifier for this module (e.g. "arp_discovery").
- Name() string
-
- // Interval returns how often the scheduler should run this module.
- // A zero duration means the module runs once on startup only.
- Interval() time.Duration
-
- // Run executes one scan cycle. It must respect ctx cancellation.
- Run(ctx context.Context) error
-}
-
-// Registry holds all registered modules.
-type Registry struct {
- modules []Module
-}
-
-// NewRegistry returns an empty module registry.
-func NewRegistry() *Registry {
- return &Registry{}
-}
-
-// Register adds a module to the registry.
-func (r *Registry) Register(m Module) {
- r.modules = append(r.modules, m)
-}
-
-// All returns all registered modules.
-func (r *Registry) All() []Module {
- return r.modules
-}
-
-// Get returns the module with the given name.
-func (r *Registry) Get(name string) (Module, error) {
- for _, m := range r.modules {
- if m.Name() == name {
- return m, nil
- }
- }
- return nil, fmt.Errorf("module %q not registered", name)
-}
diff --git a/nexus-scanner/internal/modules/adsync/adsync.go b/nexus-scanner/internal/modules/adsync/adsync.go
deleted file mode 100644
index 34dcfe0..0000000
--- a/nexus-scanner/internal/modules/adsync/adsync.go
+++ /dev/null
@@ -1,182 +0,0 @@
-// Package adsync queries Active Directory for computer objects and stores
-// them in the local database. It enriches existing hosts with their AD
-// computer name and department, and records computers that are in AD but
-// not yet seen on the network (useful for finding offline/missing devices).
-package adsync
-
-import (
- "context"
- "fmt"
- "log/slog"
- "strconv"
- "strings"
- "time"
-
- "github.com/go-ldap/ldap/v3"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// Module queries Active Directory for all computer objects.
-type Module struct {
- site string
- cfg config.ADConfig
- store *db.Store
- log *slog.Logger
-}
-
-// New returns a new AD sync module.
-func New(site string, cfg config.ADConfig, store *db.Store) *Module {
- return &Module{
- site: site,
- cfg: cfg,
- store: store,
- log: slog.With("module", "ad_sync"),
- }
-}
-
-func (m *Module) Name() string { return "ad_sync" }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-// Run executes one AD sync cycle.
-func (m *Module) Run(ctx context.Context) error {
- scanID, err := m.store.BeginScan(m.Name())
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
- scanErr := m.sync(ctx)
- if endErr := m.store.EndScan(scanID, scanErr); endErr != nil {
- m.log.Error("record scan end", "err", endErr)
- }
- return scanErr
-}
-
-func (m *Module) sync(ctx context.Context) error {
- addr := fmt.Sprintf("%s:%d", m.cfg.Server, m.cfg.Port)
-
- var conn *ldap.Conn
- var err error
- if m.cfg.TLS {
- conn, err = ldap.DialURL("ldaps://" + addr)
- } else {
- conn, err = ldap.DialURL("ldap://" + addr)
- }
- if err != nil {
- return fmt.Errorf("ldap dial %s: %w", addr, err)
- }
- defer conn.Close()
-
- if err := conn.Bind(m.cfg.BindDN, m.cfg.BindPassword); err != nil {
- return fmt.Errorf("ldap bind as %s: %w", m.cfg.BindDN, err)
- }
-
- computers, err := m.searchComputers(conn)
- if err != nil {
- return fmt.Errorf("ldap search: %w", err)
- }
-
- m.log.Info("computers found in AD", "count", len(computers))
-
- saved := 0
- for _, c := range computers {
- if err := m.store.UpsertADComputer(c); err != nil {
- m.log.Error("upsert ad computer", "cn", c.CN, "err", err)
- continue
- }
- saved++
- m.log.Debug("ad computer synced", "cn", c.CN, "os", c.OS, "dept", c.Department)
- }
-
- m.log.Info("ad sync complete", "synced", saved, "total", len(computers))
- return nil
-}
-
-func (m *Module) searchComputers(conn *ldap.Conn) ([]db.ADComputer, error) {
- req := ldap.NewSearchRequest(
- m.cfg.SearchBase,
- ldap.ScopeWholeSubtree,
- ldap.NeverDerefAliases,
- 0, 0, false,
- "(&(objectClass=computer)(objectCategory=Computer))",
- []string{
- "cn",
- "distinguishedName",
- "operatingSystem",
- "operatingSystemVersion",
- "lastLogonTimestamp",
- "whenCreated",
- "description",
- "managedBy",
- },
- nil,
- )
-
- result, err := conn.SearchWithPaging(req, 500)
- if err != nil {
- return nil, fmt.Errorf("ldap search computers: %w", err)
- }
-
- var computers []db.ADComputer
- for _, entry := range result.Entries {
- c := db.ADComputer{
- CN: entry.GetAttributeValue("cn"),
- DN: entry.GetAttributeValue("distinguishedName"),
- OS: entry.GetAttributeValue("operatingSystem"),
- OSVersion: entry.GetAttributeValue("operatingSystemVersion"),
- Owner: entry.GetAttributeValue("description"),
- Department: ouFromDN(entry.GetAttributeValue("distinguishedName")),
- }
-
- if raw := entry.GetAttributeValue("lastLogonTimestamp"); raw != "" {
- c.LastLogon = windowsTimeToGoTime(raw)
- }
- if raw := entry.GetAttributeValue("whenCreated"); raw != "" {
- c.ADCreated = parseLDAPGeneralizedTime(raw)
- }
-
- computers = append(computers, c)
- }
-
- return computers, nil
-}
-
-// ouFromDN extracts the first OU component from a distinguished name.
-// "CN=IT-NB-02,OU=Notebooks,OU=IT,DC=winkel,DC=local" → "Notebooks"
-func ouFromDN(dn string) string {
- for _, part := range strings.Split(dn, ",") {
- part = strings.TrimSpace(part)
- if strings.HasPrefix(strings.ToUpper(part), "OU=") {
- return part[3:]
- }
- }
- return ""
-}
-
-// windowsTimeToGoTime converts a Windows LDAP timestamp string (100-ns intervals
-// since 1601-01-01) to a Go time.Time.
-func windowsTimeToGoTime(raw string) time.Time {
- n, err := strconv.ParseInt(raw, 10, 64)
- if err != nil || n == 0 {
- return time.Time{}
- }
- // Offset between Windows epoch (1601-01-01) and Unix epoch (1970-01-01)
- // in 100-nanosecond intervals.
- const windowsToUnix int64 = 116444736000000000
- unixNano := (n - windowsToUnix) * 100
- return time.Unix(0, unixNano).UTC()
-}
-
-// parseLDAPGeneralizedTime parses LDAP generalizedTime format: "20240101120000.0Z"
-func parseLDAPGeneralizedTime(raw string) time.Time {
- // Trim trailing sub-second and timezone info for simple parsing.
- raw = strings.TrimSuffix(raw, "Z")
- if idx := strings.Index(raw, "."); idx != -1 {
- raw = raw[:idx]
- }
- t, err := time.Parse("20060102150405", raw)
- if err != nil {
- return time.Time{}
- }
- return t.UTC()
-}
diff --git a/nexus-scanner/internal/modules/arp/arp.go b/nexus-scanner/internal/modules/arp/arp.go
deleted file mode 100644
index 1a5efb7..0000000
--- a/nexus-scanner/internal/modules/arp/arp.go
+++ /dev/null
@@ -1,167 +0,0 @@
-// Package arp implements the arp_discovery scanner module.
-// It sends ARP requests across configured subnets and records
-// responding hosts (IP + MAC) in the shared SQLite store.
-package arp
-
-import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "net/netip"
- "time"
-
- "github.com/mdlayher/arp"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// Module performs ARP discovery on configured subnets.
-type Module struct {
- site string
- cfg config.ARPConfig
- store *db.Store
- log *slog.Logger
-}
-
-// New returns a new ARP discovery module.
-func New(site string, cfg config.ARPConfig, store *db.Store) *Module {
- return &Module{
- site: site,
- cfg: cfg,
- store: store,
- log: slog.With("module", "arp_discovery"),
- }
-}
-
-func (m *Module) Name() string { return "arp_discovery" }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-// Run executes one full ARP scan cycle across all configured subnets.
-func (m *Module) Run(ctx context.Context) error {
- scanID, err := m.store.BeginScan(m.Name())
- if err != nil {
- return fmt.Errorf("begin scan record: %w", err)
- }
-
- scanErr := m.scanAllSubnets(ctx)
-
- if err := m.store.EndScan(scanID, scanErr); err != nil {
- m.log.Error("failed to record scan end", "err", err)
- }
-
- return scanErr
-}
-
-func (m *Module) scanAllSubnets(ctx context.Context) error {
- iface, err := net.InterfaceByName(m.cfg.Interface)
- if err != nil {
- return fmt.Errorf("interface %q: %w", m.cfg.Interface, err)
- }
-
- client, err := arp.Dial(iface)
- if err != nil {
- return fmt.Errorf("arp dial on %s: %w", m.cfg.Interface, err)
- }
- defer client.Close()
-
- for _, subnet := range m.cfg.Subnets {
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- }
-
- if err := m.scanSubnet(ctx, client, subnet); err != nil {
- // Log and continue — one failed subnet should not abort others.
- m.log.Error("subnet scan failed", "subnet", subnet, "err", err)
- }
- }
- return nil
-}
-
-func (m *Module) scanSubnet(ctx context.Context, client *arp.Client, cidr string) error {
- prefix, err := netip.ParsePrefix(cidr)
- if err != nil {
- return fmt.Errorf("parse prefix %q: %w", cidr, err)
- }
- prefix = prefix.Masked()
-
- targets := hostsInPrefix(prefix)
- m.log.Info("scanning subnet", "subnet", cidr, "targets", len(targets))
-
- // Send all ARP requests first, then collect responses.
- // Set a deadline so Read() returns after silence.
- if err := client.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
- return fmt.Errorf("set deadline: %w", err)
- }
-
- for _, ip := range targets {
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- }
-
- if err := client.Request(ip); err != nil {
- m.log.Debug("arp request failed", "ip", ip, "err", err)
- }
-
- // Small delay to avoid flooding the network.
- time.Sleep(2 * time.Millisecond)
- }
-
- // Collect all responses until the deadline fires.
- found := 0
- for {
- pkt, _, err := client.Read()
- if err != nil {
- // Deadline reached — done reading.
- break
- }
-
- now := time.Now().UTC()
- host := db.Host{
- IP: pkt.SenderIP.String(),
- MAC: pkt.SenderHardwareAddr.String(),
- Site: m.site,
- FirstSeen: now,
- LastSeen: now,
- }
-
- if err := m.store.UpsertHost(host); err != nil {
- m.log.Error("upsert host", "ip", host.IP, "err", err)
- continue
- }
-
- m.log.Info("host discovered", "ip", host.IP, "mac", host.MAC)
- found++
- }
-
- m.log.Info("subnet scan done", "subnet", cidr, "found", found)
- return nil
-}
-
-// hostsInPrefix returns all usable host addresses in a prefix,
-// excluding the network address and the broadcast address.
-//
-// Example: 192.168.0.0/24 → 192.168.0.1 … 192.168.0.254
-func hostsInPrefix(prefix netip.Prefix) []netip.Addr {
- var addrs []netip.Addr
-
- // Start at network address + 1 (skip network address itself).
- addr := prefix.Masked().Addr().Next()
-
- for prefix.Contains(addr) {
- next := addr.Next()
- // Stop before the broadcast address (last address in prefix).
- if !prefix.Contains(next) {
- break
- }
- addrs = append(addrs, addr)
- addr = next
- }
-
- return addrs
-}
diff --git a/nexus-scanner/internal/modules/arp/arp_test.go b/nexus-scanner/internal/modules/arp/arp_test.go
deleted file mode 100644
index 27d5536..0000000
--- a/nexus-scanner/internal/modules/arp/arp_test.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package arp_test
-
-import (
- "fmt"
- "net/netip"
- "os"
- "testing"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// helper creates a temp SQLite store and registers cleanup.
-func newTestStore(t *testing.T) *db.Store {
- t.Helper()
-
- f, err := os.CreateTemp("", "nexus-test-*.db")
- if err != nil {
- t.Fatalf("create temp db: %v", err)
- }
- f.Close()
-
- t.Cleanup(func() { os.Remove(f.Name()) })
-
- store, err := db.Open(f.Name())
- if err != nil {
- t.Fatalf("db.Open: %v", err)
- }
- t.Cleanup(func() { store.Close() })
-
- return store
-}
-
-func TestUpsertAndListHost(t *testing.T) {
- store := newTestStore(t)
- now := time.Now().UTC().Truncate(time.Second)
-
- original := db.Host{
- IP: "192.168.0.10",
- MAC: "aa:bb:cc:dd:ee:ff",
- Vendor: "Acme Corp",
- Site: "LUD",
- FirstSeen: now,
- LastSeen: now,
- }
-
- if err := store.UpsertHost(original); err != nil {
- t.Fatalf("UpsertHost (insert): %v", err)
- }
-
- hosts, err := store.ListHosts("")
- if err != nil {
- t.Fatalf("ListHosts: %v", err)
- }
- if len(hosts) != 1 {
- t.Fatalf("expected 1 host, got %d", len(hosts))
- }
-
- h := hosts[0]
- if h.IP != original.IP {
- t.Errorf("IP: got %q, want %q", h.IP, original.IP)
- }
- if h.MAC != original.MAC {
- t.Errorf("MAC: got %q, want %q", h.MAC, original.MAC)
- }
- if h.Status != "online" {
- t.Errorf("Status: got %q, want %q", h.Status, "online")
- }
-
- // Update the same IP — last_seen and MAC should change, first_seen should not.
- updated := original
- updated.MAC = "11:22:33:44:55:66"
- updated.LastSeen = now.Add(time.Minute)
-
- if err := store.UpsertHost(updated); err != nil {
- t.Fatalf("UpsertHost (update): %v", err)
- }
-
- hosts, err = store.ListHosts("")
- if err != nil {
- t.Fatalf("ListHosts after update: %v", err)
- }
- if len(hosts) != 1 {
- t.Fatalf("expected 1 host after upsert, got %d", len(hosts))
- }
- if hosts[0].MAC != "11:22:33:44:55:66" {
- t.Errorf("MAC after update: got %q, want %q", hosts[0].MAC, "11:22:33:44:55:66")
- }
-}
-
-func TestListHostsFilterBySite(t *testing.T) {
- store := newTestStore(t)
- now := time.Now().UTC()
-
- hosts := []db.Host{
- {IP: "10.0.0.1", MAC: "aa:aa:aa:aa:aa:01", Site: "LUD", FirstSeen: now, LastSeen: now},
- {IP: "10.0.0.2", MAC: "aa:aa:aa:aa:aa:02", Site: "LUD", FirstSeen: now, LastSeen: now},
- {IP: "10.0.1.1", MAC: "aa:aa:aa:aa:bb:01", Site: "BAR", FirstSeen: now, LastSeen: now},
- }
-
- for _, h := range hosts {
- if err := store.UpsertHost(h); err != nil {
- t.Fatalf("UpsertHost %s: %v", h.IP, err)
- }
- }
-
- lud, err := store.ListHosts("LUD")
- if err != nil {
- t.Fatal(err)
- }
- if len(lud) != 2 {
- t.Errorf("LUD: expected 2, got %d", len(lud))
- }
-
- bar, err := store.ListHosts("BAR")
- if err != nil {
- t.Fatal(err)
- }
- if len(bar) != 1 {
- t.Errorf("BAR: expected 1, got %d", len(bar))
- }
-
- all, err := store.ListHosts("")
- if err != nil {
- t.Fatal(err)
- }
- if len(all) != 3 {
- t.Errorf("all: expected 3, got %d", len(all))
- }
-}
-
-func TestCountHosts(t *testing.T) {
- store := newTestStore(t)
- now := time.Now().UTC()
-
- n, err := store.CountHosts()
- if err != nil {
- t.Fatal(err)
- }
- if n != 0 {
- t.Errorf("expected 0 initially, got %d", n)
- }
-
- for i := range 5 {
- err := store.UpsertHost(db.Host{
- IP: fmt.Sprintf("10.0.0.%d", i+1),
- MAC: fmt.Sprintf("aa:bb:cc:dd:ee:%02x", i),
- Site: "LUD",
- FirstSeen: now,
- LastSeen: now,
- })
- if err != nil {
- t.Fatalf("UpsertHost %d: %v", i, err)
- }
- }
-
- n, err = store.CountHosts()
- if err != nil {
- t.Fatal(err)
- }
- if n != 5 {
- t.Errorf("expected 5, got %d", n)
- }
-}
-
-func TestScanRunLifecycle(t *testing.T) {
- store := newTestStore(t)
-
- id, err := store.BeginScan("arp_discovery")
- if err != nil {
- t.Fatalf("BeginScan: %v", err)
- }
- if id == 0 {
- t.Error("expected non-zero scan ID")
- }
-
- if err := store.EndScan(id, nil); err != nil {
- t.Fatalf("EndScan (ok): %v", err)
- }
-
- last, err := store.LastScanTime("arp_discovery")
- if err != nil {
- t.Fatal(err)
- }
- if last.IsZero() {
- t.Error("expected non-zero last scan time after successful run")
- }
-}
-
-// TestHostsInPrefix validates the subnet host enumeration logic.
-// This test lives here because hostsInPrefix is package-internal;
-// in a real scenario you would export it for testing or white-box test it.
-func TestHostsInPrefixCount(t *testing.T) {
- cases := []struct {
- cidr string
- count int
- }{
- {"192.168.0.0/24", 254}, // .1 – .254
- {"10.0.0.0/30", 2}, // .1 and .2 only
- {"10.0.0.0/29", 6}, // .1 – .6
- }
-
- for _, tc := range cases {
- t.Run(tc.cidr, func(t *testing.T) {
- prefix, err := netip.ParsePrefix(tc.cidr)
- if err != nil {
- t.Fatal(err)
- }
- got := hostsInPrefix(prefix.Masked())
- if len(got) != tc.count {
- t.Errorf("cidr %s: got %d hosts, want %d", tc.cidr, len(got), tc.count)
- }
- })
- }
-}
-
-// hostsInPrefix is a copy of the unexported function for white-box testing.
-func hostsInPrefix(prefix netip.Prefix) []netip.Addr {
- var addrs []netip.Addr
- addr := prefix.Masked().Addr().Next()
- for prefix.Contains(addr) {
- next := addr.Next()
- if !prefix.Contains(next) {
- break
- }
- addrs = append(addrs, addr)
- addr = next
- }
- return addrs
-}
diff --git a/nexus-scanner/internal/modules/dnsreverse/dnsreverse.go b/nexus-scanner/internal/modules/dnsreverse/dnsreverse.go
deleted file mode 100644
index 7fd9666..0000000
--- a/nexus-scanner/internal/modules/dnsreverse/dnsreverse.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Package dnsreverse implements the dns_reverse module.
-// It enriches already-known hosts by performing reverse-DNS lookups
-// for every host that has no hostname recorded yet.
-package dnsreverse
-
-import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "strings"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// Module enriches hosts in the database with reverse-DNS hostnames.
-type Module struct {
- site string
- store *db.Store
- log *slog.Logger
-}
-
-// New returns a new dns_reverse module.
-func New(site string, store *db.Store) *Module {
- return &Module{
- site: site,
- store: store,
- log: slog.With("module", "dns_reverse"),
- }
-}
-
-func (m *Module) Name() string { return "dns_reverse" }
-func (m *Module) Interval() time.Duration { return 10 * time.Minute }
-
-// Run looks up hostnames for all hosts that have none recorded yet.
-func (m *Module) Run(ctx context.Context) error {
- scanID, err := m.store.BeginScan(m.Name())
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- scanErr := m.scan(ctx)
- if endErr := m.store.EndScan(scanID, scanErr); endErr != nil {
- m.log.Error("record scan end", "err", endErr)
- }
- return scanErr
-}
-
-func (m *Module) scan(ctx context.Context) error {
- hosts, err := m.store.ListHosts("")
- if err != nil {
- return fmt.Errorf("list hosts: %w", err)
- }
-
- resolved := 0
- for _, h := range hosts {
- // Skip hosts that already have a hostname.
- if h.Hostname != "" {
- continue
- }
-
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- }
-
- names, err := net.DefaultResolver.LookupAddr(ctx, h.IP)
- if err != nil {
- // A lookup failure is not fatal — continue with the next host.
- m.log.Debug("reverse lookup failed", "ip", h.IP, "err", err)
- continue
- }
- if len(names) == 0 {
- continue
- }
-
- // LookupAddr returns FQDNs with a trailing dot — strip it.
- hostname := strings.TrimSuffix(names[0], ".")
-
- h.Hostname = hostname
- if err := m.store.UpsertHost(h); err != nil {
- m.log.Error("upsert host", "ip", h.IP, "err", err)
- continue
- }
-
- m.log.Info("hostname resolved", "ip", h.IP, "hostname", hostname)
- resolved++
- }
-
- m.log.Info("scan complete", "checked", len(hosts), "resolved", resolved)
- return nil
-}
diff --git a/nexus-scanner/internal/modules/macvendor/macvendor.go b/nexus-scanner/internal/modules/macvendor/macvendor.go
deleted file mode 100644
index 4526a2d..0000000
--- a/nexus-scanner/internal/modules/macvendor/macvendor.go
+++ /dev/null
@@ -1,129 +0,0 @@
-// Package macvendor implements the mac_vendor module.
-// It maps the OUI (first 3 bytes) of every known host's MAC address to a
-// human-readable vendor name and persists the result in the database.
-// The mapping is based on a built-in table; no external files are required.
-package macvendor
-
-import (
- "context"
- "fmt"
- "log/slog"
- "strings"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// ouiTable maps lowercase OUI prefixes (aa:bb:cc) to vendor names.
-// Broadcast / multicast entries are intentionally kept with an empty string
-// so they are recognised but not written to the database.
-var ouiTable = map[string]string{
- "00:0c:29": "VMware",
- "00:50:56": "VMware",
- "bc:24:11": "Proxmox/Ceph",
- "90:1b:0e": "Supermicro",
- "90:09:d0": "Synology",
- "00:17:c8": "Hewlett-Packard",
- "00:be:43": "Ubiquiti",
- "e4:43:4b": "Ubiquiti",
- "24:6a:0e": "Ubiquiti",
- "40:86:cb": "Intel",
- "68:c6:ac": "Intel",
- "1c:af:4a": "Dell",
- "50:81:40": "Dell",
- "4c:5f:70": "Lenovo",
- "dc:58:bc": "Apple",
- "c8:4b:d6": "Kyocera",
- "38:d5:7a": "Samsung",
- "00:04:a5": "Barco",
- "7c:5a:1c": "LANCOM",
- "00:0a:b3": "Cisco",
- "00:e0:67": "Aten",
- "ff:ff:ff": "", // broadcast — leave vendor empty
-}
-
-// LookupVendor returns the vendor name for a MAC address.
-// The MAC must be in the format aa:bb:cc:dd:ee:ff (colon-separated).
-// An empty string is returned when no match is found.
-func LookupVendor(mac string) string {
- if len(mac) < 8 {
- return ""
- }
- oui := strings.ToLower(mac[:8])
- return ouiTable[oui]
-}
-
-// Module enriches hosts in the database with OUI-based vendor names.
-type Module struct {
- site string
- store *db.Store
- log *slog.Logger
-}
-
-// New returns a new mac_vendor module.
-func New(site string, store *db.Store) *Module {
- return &Module{
- site: site,
- store: store,
- log: slog.With("module", "mac_vendor"),
- }
-}
-
-func (m *Module) Name() string { return "mac_vendor" }
-
-// Interval returns 0, which causes the scheduler to run this module once at
-// startup and never repeat it automatically.
-func (m *Module) Interval() time.Duration { return 0 }
-
-// Run assigns vendor names to all hosts that have none recorded yet.
-func (m *Module) Run(ctx context.Context) error {
- scanID, err := m.store.BeginScan(m.Name())
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- scanErr := m.scan(ctx)
- if endErr := m.store.EndScan(scanID, scanErr); endErr != nil {
- m.log.Error("record scan end", "err", endErr)
- }
- return scanErr
-}
-
-func (m *Module) scan(ctx context.Context) error {
- hosts, err := m.store.ListHosts("")
- if err != nil {
- return fmt.Errorf("list hosts: %w", err)
- }
-
- enriched := 0
- for _, h := range hosts {
- // Skip hosts that already have a vendor assigned.
- if h.Vendor != "" {
- continue
- }
-
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- }
-
- vendor := LookupVendor(h.MAC)
- if vendor == "" {
- // No entry in our table — leave vendor empty.
- continue
- }
-
- h.Vendor = vendor
- if err := m.store.UpsertHost(h); err != nil {
- m.log.Error("upsert host", "ip", h.IP, "err", err)
- continue
- }
-
- m.log.Info("vendor resolved", "ip", h.IP, "mac", h.MAC, "vendor", vendor)
- enriched++
- }
-
- m.log.Info("scan complete", "checked", len(hosts), "enriched", enriched)
- return nil
-}
diff --git a/nexus-scanner/internal/modules/macvendor/macvendor_test.go b/nexus-scanner/internal/modules/macvendor/macvendor_test.go
deleted file mode 100644
index 2f3ac6e..0000000
--- a/nexus-scanner/internal/modules/macvendor/macvendor_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package macvendor
-
-import "testing"
-
-func TestLookupVendor(t *testing.T) {
- tests := []struct {
- mac string
- want string
- }{
- // Known OUIs from the built-in table.
- {"00:0c:29:1a:2b:3c", "VMware"},
- {"00:50:56:ab:cd:ef", "VMware"},
- {"bc:24:11:00:00:01", "Proxmox/Ceph"},
- {"90:1b:0e:ff:ee:dd", "Supermicro"},
- {"90:09:d0:11:22:33", "Synology"},
- {"00:17:c8:44:55:66", "Hewlett-Packard"},
- {"00:be:43:77:88:99", "Ubiquiti"},
- {"e4:43:4b:aa:bb:cc", "Ubiquiti"},
- {"24:6a:0e:dd:ee:ff", "Ubiquiti"},
- {"40:86:cb:00:11:22", "Intel"},
- {"68:c6:ac:33:44:55", "Intel"},
- {"1c:af:4a:66:77:88", "Dell"},
- {"50:81:40:99:aa:bb", "Dell"},
- {"4c:5f:70:cc:dd:ee", "Lenovo"},
- {"dc:58:bc:ff:00:11", "Apple"},
- {"c8:4b:d6:22:33:44", "Kyocera"},
- {"38:d5:7a:55:66:77", "Samsung"},
- {"00:04:a5:88:99:aa", "Barco"},
- {"7c:5a:1c:bb:cc:dd", "LANCOM"},
- {"00:0a:b3:ee:ff:00", "Cisco"},
- {"00:e0:67:11:22:33", "Aten"},
-
- // Broadcast — vendor must be empty.
- {"ff:ff:ff:ff:ff:ff", ""},
-
- // Unknown OUI — no vendor.
- {"de:ad:be:ef:00:01", ""},
-
- // MAC shorter than 8 characters — must not panic.
- {"aa:bb", ""},
- {"", ""},
-
- // Upper-case input — must be normalised.
- {"00:0C:29:1A:2B:3C", "VMware"},
- {"BC:24:11:FF:EE:DD", "Proxmox/Ceph"},
- }
-
- for _, tc := range tests {
- got := LookupVendor(tc.mac)
- if got != tc.want {
- t.Errorf("LookupVendor(%q) = %q, want %q", tc.mac, got, tc.want)
- }
- }
-}
diff --git a/nexus-scanner/internal/modules/nexusreporter/reporter.go b/nexus-scanner/internal/modules/nexusreporter/reporter.go
deleted file mode 100644
index 744e5ec..0000000
--- a/nexus-scanner/internal/modules/nexusreporter/reporter.go
+++ /dev/null
@@ -1,130 +0,0 @@
-// Package nexusreporter pushes newly discovered or changed assets to IT Nexus.
-package nexusreporter
-
-import (
- "bytes"
- "context"
- "crypto/sha256"
- "encoding/json"
- "fmt"
- "log/slog"
- "net/http"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-const modName = "nexus_reporter"
-
-type Module struct {
- site string
- cfg config.ReporterConfig
- nexus config.Config // full config for URL + APIKey
- store *db.Store
- client *http.Client
- // last hash per IP to detect changes
- lastHash map[string][32]byte
-}
-
-// New creates a new IT Nexus reporter module.
-func New(site string, cfg config.ReporterConfig, nexusCfg config.Config, store *db.Store) *Module {
- return &Module{
- site: site,
- cfg: cfg,
- nexus: nexusCfg,
- store: store,
- client: &http.Client{Timeout: 15 * time.Second},
- lastHash: make(map[string][32]byte),
- }
-}
-
-func (m *Module) Name() string { return modName }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-type assetPayload struct {
- Site string `json:"site"`
- Version string `json:"scanner_version"`
- ReportedAt time.Time `json:"reported_at"`
- Assets []assetItem `json:"assets"`
-}
-
-type assetItem struct {
- IP string `json:"ip"`
- MAC string `json:"mac"`
- Hostname string `json:"hostname"`
- Vendor string `json:"vendor"`
- Status string `json:"status"`
- FirstSeen time.Time `json:"first_seen"`
- LastSeen time.Time `json:"last_seen"`
-}
-
-// Run reports changed/new hosts to IT Nexus.
-func (m *Module) Run(ctx context.Context) error {
- if m.nexus.Nexus.URL == "" || m.nexus.Nexus.APIKey == "" {
- slog.Debug("nexus reporter skipped — URL or APIKey not configured")
- return nil
- }
-
- hosts, err := m.store.ListHosts("")
- if err != nil {
- return fmt.Errorf("list hosts: %w", err)
- }
-
- var changed []assetItem
- for _, h := range hosts {
- hash := hostHash(h)
- if prev, seen := m.lastHash[h.IP]; seen && prev == hash {
- continue // unchanged
- }
- m.lastHash[h.IP] = hash
- changed = append(changed, assetItem{
- IP: h.IP,
- MAC: h.MAC,
- Hostname: h.Hostname,
- Vendor: h.Vendor,
- Status: h.Status,
- FirstSeen: h.FirstSeen,
- LastSeen: h.LastSeen,
- })
- }
-
- if len(changed) == 0 {
- slog.Debug("nexus reporter: no changes")
- return nil
- }
-
- payload := assetPayload{
- Site: m.site,
- Version: "1.0",
- ReportedAt: time.Now(),
- Assets: changed,
- }
-
- body, _ := json.Marshal(payload)
- url := m.nexus.Nexus.URL + "/api/scanner/assets"
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
- if err != nil {
- return fmt.Errorf("build request: %w", err)
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Scanner-Key", m.nexus.Nexus.APIKey)
-
- resp, err := m.client.Do(req)
- if err != nil {
- return fmt.Errorf("post to nexus: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode >= 400 {
- return fmt.Errorf("nexus returned HTTP %d", resp.StatusCode)
- }
-
- slog.Info("nexus reporter: assets reported", "count", len(changed), "url", url)
- return nil
-}
-
-func hostHash(h db.Host) [32]byte {
- s := h.IP + "|" + h.MAC + "|" + h.Hostname + "|" + h.Vendor + "|" + h.Status
- return sha256.Sum256([]byte(s))
-}
diff --git a/nexus-scanner/internal/modules/portscan/portscan.go b/nexus-scanner/internal/modules/portscan/portscan.go
deleted file mode 100644
index 0eb3e37..0000000
--- a/nexus-scanner/internal/modules/portscan/portscan.go
+++ /dev/null
@@ -1,106 +0,0 @@
-// Package portscan performs concurrent TCP port scanning on all online hosts.
-package portscan
-
-import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "sort"
- "sync"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-const modName = "port_scan"
-
-// Common ports to scan when none are configured.
-var defaultPorts = []int{21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 3389, 5985, 8080, 8443, 9100}
-
-// Module scans TCP ports on all online hosts.
-type Module struct {
- site string
- cfg config.PortScanConfig
- store *db.Store
-}
-
-// New creates a new port scan module.
-func New(site string, cfg config.PortScanConfig, store *db.Store) *Module {
- return &Module{site: site, cfg: cfg, store: store}
-}
-
-func (m *Module) Name() string { return modName }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-// Run scans all online hosts and stores their open ports.
-func (m *Module) Run(ctx context.Context) error {
- hosts, err := m.store.ListHosts("")
- if err != nil {
- return fmt.Errorf("list hosts: %w", err)
- }
-
- ports := m.cfg.Ports
- if len(ports) == 0 {
- ports = defaultPorts
- }
- timeout := m.cfg.Timeout
- if timeout == 0 {
- timeout = 500 * time.Millisecond
- }
-
- scanID, err := m.store.BeginScan(modName)
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- found := 0
- for _, h := range hosts {
- if h.Status != "online" {
- continue
- }
- open := scanHost(ctx, h.IP, ports, timeout)
- sort.Ints(open)
- if err := m.store.UpsertHostPorts(h.IP, open); err != nil {
- slog.Error("save host ports", "ip", h.IP, "err", err)
- }
- if len(open) > 0 {
- slog.Debug("port scan", "ip", h.IP, "open", open)
- found++
- }
- }
-
- _ = m.store.EndScan(scanID, nil)
- slog.Info("port scan complete", "hosts_with_open_ports", found, "total_hosts", len(hosts))
- return nil
-}
-
-// scanHost tries all ports concurrently (max 30 goroutines) and returns open ones.
-func scanHost(ctx context.Context, ip string, ports []int, timeout time.Duration) []int {
- var mu sync.Mutex
- var open []int
- var wg sync.WaitGroup
- sem := make(chan struct{}, 30)
-
- for _, port := range ports {
- wg.Add(1)
- go func(p int) {
- defer wg.Done()
- sem <- struct{}{}
- defer func() { <-sem }()
-
- addr := fmt.Sprintf("%s:%d", ip, p)
- d := net.Dialer{Timeout: timeout}
- conn, err := d.DialContext(ctx, "tcp", addr)
- if err == nil {
- conn.Close()
- mu.Lock()
- open = append(open, p)
- mu.Unlock()
- }
- }(port)
- }
- wg.Wait()
- return open
-}
diff --git a/nexus-scanner/internal/modules/sitemon/sitemon.go b/nexus-scanner/internal/modules/sitemon/sitemon.go
deleted file mode 100644
index e584151..0000000
--- a/nexus-scanner/internal/modules/sitemon/sitemon.go
+++ /dev/null
@@ -1,298 +0,0 @@
-// Package sitemon checks the reachability of configured targets (ping, http, tcp).
-// Each scanner instance monitors from its own site perspective, giving you
-// per-site uptime visibility across all your locations.
-package sitemon
-
-import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/json"
- "fmt"
- "log/slog"
- "net"
- "net/http"
- "net/smtp"
- "os/exec"
- "runtime"
- "strings"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-const modName = "site_monitoring"
-
-// Module runs periodic availability checks and stores results in the DB.
-type Module struct {
- site string
- cfg config.SiteMonConfig
- alertCfg config.AlertConfig
- nexusCfg config.Config
- store *db.Store
- client *http.Client
- alertHTTP *http.Client
- // track previous state per check ID to detect transitions
- prevState map[int64]string
-}
-
-// New creates a new site monitoring module.
-func New(site string, cfg config.SiteMonConfig, alertCfg config.AlertConfig, nexusCfg config.Config, store *db.Store) *Module {
- return &Module{
- site: site,
- cfg: cfg,
- alertCfg: alertCfg,
- nexusCfg: nexusCfg,
- store: store,
- prevState: make(map[int64]string),
- alertHTTP: &http.Client{Timeout: 10 * time.Second},
- client: &http.Client{
- Timeout: 10 * time.Second,
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- if len(via) >= 5 {
- return fmt.Errorf("too many redirects")
- }
- return nil
- },
- },
- }
-}
-
-func (m *Module) Name() string { return modName }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-// Run executes all enabled checks once and stores results.
-func (m *Module) Run(ctx context.Context) error {
- checks, err := m.store.ListMonitorChecks()
- if err != nil {
- return fmt.Errorf("list checks: %w", err)
- }
- if len(checks) == 0 {
- return nil
- }
-
- scanID, err := m.store.BeginScan(modName)
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- for _, check := range checks {
- if !check.Enabled {
- continue
- }
- result := m.runCheck(ctx, check)
- if err := m.store.InsertMonitorResult(result); err != nil {
- slog.Error("insert monitor result", "check", check.Name, "err", err)
- }
- slog.Debug("monitor check done",
- "name", check.Name, "status", result.Status, "latency_ms", result.LatencyMS)
-
- // Detect state transitions and fire alerts.
- if prev, seen := m.prevState[check.ID]; seen && prev != result.Status {
- go m.sendAlert(check, result)
- }
- m.prevState[check.ID] = result.Status
- }
-
- _ = m.store.EndScan(scanID, nil)
- _ = m.store.CleanupMonitorResults(100)
- return nil
-}
-
-func (m *Module) runCheck(ctx context.Context, check db.MonitorCheck) db.MonitorResult {
- result := db.MonitorResult{
- CheckID: check.ID,
- CheckedAt: time.Now(),
- }
-
- start := time.Now()
- var checkErr error
-
- switch check.Type {
- case "ping":
- checkErr = doPing(ctx, check.Target)
- case "http", "https":
- checkErr = doHTTP(ctx, m.client, check.Target)
- case "tcp":
- checkErr = doTCP(ctx, check.Target)
- default:
- checkErr = fmt.Errorf("unknown type: %s", check.Type)
- }
-
- result.LatencyMS = int(time.Since(start).Milliseconds())
- if checkErr != nil {
- result.Status = "offline"
- result.Error = checkErr.Error()
- } else {
- result.Status = "online"
- }
- return result
-}
-
-func doPing(ctx context.Context, target string) error {
- var args []string
- if runtime.GOOS == "windows" {
- args = []string{"-n", "1", "-w", "2000", target}
- } else {
- args = []string{"-c", "1", "-W", "2", target}
- }
- cmd := exec.CommandContext(ctx, "ping", args...)
- out, err := cmd.Output()
- if err != nil {
- return fmt.Errorf("ping: %w", err)
- }
- lower := strings.ToLower(string(out))
- if strings.Contains(lower, "unreachable") ||
- strings.Contains(lower, "timed out") ||
- strings.Contains(lower, "100% loss") ||
- strings.Contains(lower, "100% packet loss") {
- return fmt.Errorf("host unreachable")
- }
- return nil
-}
-
-func doHTTP(ctx context.Context, client *http.Client, target string) error {
- if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
- target = "https://" + target
- }
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
- if err != nil {
- return err
- }
- req.Header.Set("User-Agent", "NexusScanner/1.0")
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
- resp.Body.Close()
- if resp.StatusCode >= 500 {
- return fmt.Errorf("HTTP %d", resp.StatusCode)
- }
- return nil
-}
-
-func doTCP(ctx context.Context, target string) error {
- d := net.Dialer{Timeout: 5 * time.Second}
- conn, err := d.DialContext(ctx, "tcp", target)
- if err != nil {
- return err
- }
- conn.Close()
- return nil
-}
-
-/* ── Alerts ───────────────────────────────────────────────────────── */
-
-func (m *Module) sendAlert(check db.MonitorCheck, result db.MonitorResult) {
- emoji := "🔴"
- word := "OFFLINE"
- if result.Status == "online" {
- emoji = "✅"
- word = "WIEDER ONLINE"
- }
- subject := fmt.Sprintf("[Nexus Scanner %s] %s %s: %s", m.site, emoji, word, check.Name)
- body := fmt.Sprintf(
- "Standort: %s\nCheck: %s (%s)\nZiel: %s\nStatus: %s\nZeit: %s\n",
- m.site, check.Name, check.Type, check.Target,
- strings.ToUpper(result.Status),
- result.CheckedAt.Format("02.01.2006 15:04:05"),
- )
- if result.Error != "" {
- body += "Fehler: " + result.Error + "\n"
- }
-
- if m.alertCfg.SMTP.Enabled {
- if err := m.sendMail(subject, body); err != nil {
- slog.Error("alert mail failed", "err", err)
- } else {
- slog.Info("alert mail sent", "check", check.Name, "status", result.Status)
- }
- }
-
- if m.alertCfg.NexusEnabled && m.nexusCfg.Nexus.URL != "" {
- if err := m.sendNexusAlert(check, result, subject, body); err != nil {
- slog.Error("nexus alert failed", "err", err)
- } else {
- slog.Info("nexus alert sent", "check", check.Name, "status", result.Status)
- }
- }
-}
-
-func (m *Module) sendMail(subject, body string) error {
- sc := m.alertCfg.SMTP
- addr := fmt.Sprintf("%s:%d", sc.Host, sc.Port)
- msg := []byte("From: " + sc.From + "\r\n" +
- "To: " + sc.To + "\r\n" +
- "Subject: " + subject + "\r\n" +
- "Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
- body)
-
- var auth smtp.Auth
- if sc.Username != "" {
- auth = smtp.PlainAuth("", sc.Username, sc.Password, sc.Host)
- }
-
- // Try STARTTLS first, fall back to plain.
- tlsCfg := &tls.Config{ServerName: sc.Host, InsecureSkipVerify: false}
- c, err := smtp.Dial(addr)
- if err != nil {
- return fmt.Errorf("dial smtp: %w", err)
- }
- defer c.Close()
- if ok, _ := c.Extension("STARTTLS"); ok {
- if err := c.StartTLS(tlsCfg); err != nil {
- return fmt.Errorf("starttls: %w", err)
- }
- }
- if auth != nil {
- if err := c.Auth(auth); err != nil {
- return fmt.Errorf("smtp auth: %w", err)
- }
- }
- if err := c.Mail(sc.From); err != nil {
- return err
- }
- if err := c.Rcpt(sc.To); err != nil {
- return err
- }
- wc, err := c.Data()
- if err != nil {
- return err
- }
- defer wc.Close()
- _, err = wc.Write(msg)
- return err
-}
-
-func (m *Module) sendNexusAlert(check db.MonitorCheck, result db.MonitorResult, subject, body string) error {
- payload := map[string]any{
- "site": m.site,
- "check": check.Name,
- "type": check.Type,
- "target": check.Target,
- "status": result.Status,
- "latency": result.LatencyMS,
- "error": result.Error,
- "timestamp": result.CheckedAt,
- "subject": subject,
- "body": body,
- }
- b, _ := json.Marshal(payload)
- url := m.nexusCfg.Nexus.URL + "/api/scanner/alert"
- req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Scanner-Key", m.nexusCfg.Nexus.APIKey)
- resp, err := m.alertHTTP.Do(req)
- if err != nil {
- return err
- }
- resp.Body.Close()
- if resp.StatusCode >= 400 {
- return fmt.Errorf("nexus returned %d", resp.StatusCode)
- }
- return nil
-}
diff --git a/nexus-scanner/internal/modules/snmpmod/snmp.go b/nexus-scanner/internal/modules/snmpmod/snmp.go
deleted file mode 100644
index 1d0dd11..0000000
--- a/nexus-scanner/internal/modules/snmpmod/snmp.go
+++ /dev/null
@@ -1,334 +0,0 @@
-// Package snmpmod polls SNMP targets for device information.
-// Supports generic devices, printers (toner/pages) and switches.
-package snmpmod
-
-import (
- "context"
- "fmt"
- "log/slog"
- "strconv"
- "strings"
- "time"
-
- "github.com/gosnmp/gosnmp"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-const modName = "snmp"
-
-// OIDs queried for every device.
-var baseOIDs = map[string]string{
- "sysDescr": "1.3.6.1.2.1.1.1.0",
- "sysUpTime": "1.3.6.1.2.1.1.3.0",
- "sysName": "1.3.6.1.2.1.1.5.0",
- "sysLocation": "1.3.6.1.2.1.1.6.0",
- "sysContact": "1.3.6.1.2.1.1.4.0",
- "ifNumber": "1.3.6.1.2.1.2.1.0",
-}
-
-// Switch-specific scalar OIDs (queried via Get).
-var switchScalarOIDs = map[string]string{
- "cpuLoad": "1.3.6.1.2.1.25.3.3.1.2.1", // hrProcessorLoad (first CPU)
-}
-
-// Printer-specific OIDs (Kyocera / most RFC 3805 printers).
-var printerOIDs = map[string]string{
- "tonerLevel": "1.3.6.1.2.1.43.11.1.1.9.1.1",
- "tonerMax": "1.3.6.1.2.1.43.11.1.1.8.1.1",
- "pageCount": "1.3.6.1.2.1.43.10.2.1.4.1.1",
- "printerStatus": "1.3.6.1.2.1.25.3.5.1.1.1",
-}
-
-// Walk base OIDs for interfaces and memory.
-const (
- oidIfOperStatus = "1.3.6.1.2.1.2.2.1.8" // 1=up, 2=down
- oidIfHCInOctets = "1.3.6.1.2.1.31.1.1.1.6" // 64-bit in bytes
- oidIfHCOutOctets = "1.3.6.1.2.1.31.1.1.1.10" // 64-bit out bytes
- oidIfDescr = "1.3.6.1.2.1.2.2.1.2" // interface name
- oidHrStorageDescr = "1.3.6.1.2.1.25.2.3.1.2"
- oidHrStorageUsed = "1.3.6.1.2.1.25.2.3.1.6"
- oidHrStorageSize = "1.3.6.1.2.1.25.2.3.1.5"
- oidHrStorageAlloc = "1.3.6.1.2.1.25.2.3.1.4"
-)
-
-// Module polls SNMP targets and stores results.
-type Module struct {
- site string
- cfg config.SNMPConfig
- store *db.Store
-}
-
-// New creates a new SNMP module.
-func New(site string, cfg config.SNMPConfig, store *db.Store) *Module {
- return &Module{site: site, cfg: cfg, store: store}
-}
-
-func (m *Module) Name() string { return modName }
-func (m *Module) Interval() time.Duration { return m.cfg.Interval }
-
-// Run polls all enabled SNMP targets.
-func (m *Module) Run(ctx context.Context) error {
- targets, err := m.store.ListSNMPTargets()
- if err != nil {
- return fmt.Errorf("list snmp targets: %w", err)
- }
- if len(targets) == 0 {
- return nil
- }
-
- scanID, err := m.store.BeginScan(modName)
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- ok := 0
- for _, t := range targets {
- if !t.Enabled {
- continue
- }
- if err := m.pollTarget(ctx, t); err != nil {
- slog.Warn("snmp poll failed", "target", t.Name, "ip", t.IP, "err", err)
- } else {
- ok++
- }
- }
-
- _ = m.store.EndScan(scanID, nil)
- slog.Info("snmp poll complete", "ok", ok, "total", len(targets))
- return nil
-}
-
-func (m *Module) pollTarget(ctx context.Context, t db.SNMPTarget) error {
- community := t.Community
- if community == "" {
- community = m.cfg.Community
- }
-
- gs := &gosnmp.GoSNMP{
- Target: t.IP,
- Port: 161,
- Community: community,
- Version: snmpVersion(t.Version),
- Timeout: 10 * time.Second,
- Retries: 1,
- MaxOids: 60,
- }
- if err := gs.Connect(); err != nil {
- return fmt.Errorf("connect: %w", err)
- }
- defer gs.Conn.Close()
-
- now := time.Now()
-
- // --- Base + type-specific scalar GET ---
- oids := make(map[string]string)
- for k, v := range baseOIDs {
- oids[k] = v
- }
- switch t.Type {
- case "printer":
- for k, v := range printerOIDs {
- oids[k] = v
- }
- case "switch":
- for k, v := range switchScalarOIDs {
- oids[k] = v
- }
- }
-
- oidList := make([]string, 0, len(oids))
- reverseMap := make(map[string]string)
- for name, oid := range oids {
- oidList = append(oidList, oid)
- reverseMap[oid] = name
- }
-
- result, err := gs.Get(oidList)
- if err != nil {
- return fmt.Errorf("get: %w", err)
- }
-
- for _, pdu := range result.Variables {
- name := resolveOID(pdu.Name, reverseMap)
- if name == "" {
- continue
- }
- value := pduToString(pdu)
- if value == "" || value == "" || strings.HasPrefix(value, "") {
- continue
- }
- _ = m.store.UpsertSNMPResult(db.SNMPResult{
- TargetID: t.ID,
- OIDName: name,
- Value: value,
- ScannedAt: now,
- })
- }
-
- // --- Switch: walk interface table + memory ---
- if t.Type == "switch" {
- m.walkSwitch(gs, t, now)
- }
-
- slog.Debug("snmp polled", "target", t.Name, "ip", t.IP, "type", t.Type)
- return nil
-}
-
-// walkSwitch collects interface stats and memory for switch-type targets.
-func (m *Module) walkSwitch(gs *gosnmp.GoSNMP, t db.SNMPTarget, now time.Time) {
- // Interface status walk → portsUp / portsTotal
- var portsUp, portsTotal int
- var trafficIn, trafficOut uint64
-
- ifStatus := walkOID(gs, oidIfOperStatus)
- for _, v := range ifStatus {
- portsTotal++
- if v == "1" {
- portsUp++
- }
- }
-
- // Traffic counters (64-bit)
- for _, v := range walkOID(gs, oidIfHCInOctets) {
- n, _ := strconv.ParseUint(v, 10, 64)
- trafficIn += n
- }
- for _, v := range walkOID(gs, oidIfHCOutOctets) {
- n, _ := strconv.ParseUint(v, 10, 64)
- trafficOut += n
- }
-
- save := func(name, value string) {
- _ = m.store.UpsertSNMPResult(db.SNMPResult{
- TargetID: t.ID,
- OIDName: name,
- Value: value,
- ScannedAt: now,
- })
- }
-
- if portsTotal > 0 {
- save("portsTotal", strconv.Itoa(portsTotal))
- save("portsUp", strconv.Itoa(portsUp))
- }
- if trafficIn > 0 {
- save("trafficIn", strconv.FormatUint(trafficIn, 10))
- }
- if trafficOut > 0 {
- save("trafficOut", strconv.FormatUint(trafficOut, 10))
- }
-
- // Memory: find "Physical Memory" or "Real Memory" row
- descrByIdx := walkOID(gs, oidHrStorageDescr)
- usedByIdx := walkOID(gs, oidHrStorageUsed)
- sizeByIdx := walkOID(gs, oidHrStorageSize)
- allocByIdx := walkOID(gs, oidHrStorageAlloc)
-
- for idx, descr := range descrByIdx {
- d := strings.ToLower(descr)
- if strings.Contains(d, "physical") || strings.Contains(d, "real") || strings.Contains(d, "ram") {
- used, _ := strconv.ParseInt(usedByIdx[idx], 10, 64)
- size, _ := strconv.ParseInt(sizeByIdx[idx], 10, 64)
- alloc, _ := strconv.ParseInt(allocByIdx[idx], 10, 64)
- if alloc <= 0 {
- alloc = 1024
- }
- usedBytes := used * alloc
- totalBytes := size * alloc
- if totalBytes > 0 {
- save("memUsed", strconv.FormatInt(usedBytes, 10))
- save("memTotal", strconv.FormatInt(totalBytes, 10))
- }
- break
- }
- }
-}
-
-// walkOID performs an SNMP walk and returns a map of last-OID-index → value string.
-func walkOID(gs *gosnmp.GoSNMP, baseOID string) map[string]string {
- out := make(map[string]string)
- err := gs.Walk(baseOID, func(pdu gosnmp.SnmpPDU) error {
- // Extract the index (last component of OID)
- name := strings.TrimPrefix(pdu.Name, ".")
- parts := strings.Split(name, ".")
- idx := parts[len(parts)-1]
- val := pduRawToString(pdu)
- if val != "" {
- out[idx] = val
- }
- return nil
- })
- if err != nil {
- slog.Debug("snmp walk failed", "oid", baseOID, "err", err)
- }
- return out
-}
-
-func resolveOID(oidName string, reverseMap map[string]string) string {
- key := strings.TrimPrefix(oidName, ".")
- if name := reverseMap[key]; name != "" {
- return name
- }
- // Try without trailing .0
- if len(key) > 2 && key[len(key)-2:] == ".0" {
- return reverseMap[key[:len(key)-2]]
- }
- return ""
-}
-
-func snmpVersion(v string) gosnmp.SnmpVersion {
- switch v {
- case "v1":
- return gosnmp.Version1
- case "v3":
- return gosnmp.Version3
- default:
- return gosnmp.Version2c
- }
-}
-
-// pduToString converts a PDU value to a display string (formatted).
-func pduToString(pdu gosnmp.SnmpPDU) string {
- switch pdu.Type {
- case gosnmp.OctetString:
- if b, ok := pdu.Value.([]byte); ok {
- return strings.TrimSpace(string(b))
- }
- case gosnmp.TimeTicks:
- ticks, _ := pdu.Value.(uint32)
- d := time.Duration(ticks) * 10 * time.Millisecond
- h := int(d.Hours())
- mn := int(d.Minutes()) % 60
- return strconv.Itoa(h) + "h " + strconv.Itoa(mn) + "m"
- case gosnmp.ObjectIdentifier:
- if s, ok := pdu.Value.(string); ok {
- return s
- }
- default:
- return fmt.Sprintf("%v", pdu.Value)
- }
- return fmt.Sprintf("%v", pdu.Value)
-}
-
-// pduRawToString converts a PDU value to a raw numeric/string value (for walks).
-func pduRawToString(pdu gosnmp.SnmpPDU) string {
- switch pdu.Type {
- case gosnmp.OctetString:
- if b, ok := pdu.Value.([]byte); ok {
- return strings.TrimSpace(string(b))
- }
- case gosnmp.Integer:
- return fmt.Sprintf("%d", pdu.Value)
- case gosnmp.Counter32, gosnmp.Gauge32, gosnmp.Uinteger32:
- return fmt.Sprintf("%d", pdu.Value)
- case gosnmp.Counter64:
- return fmt.Sprintf("%d", pdu.Value)
- case gosnmp.TimeTicks:
- if t, ok := pdu.Value.(uint32); ok {
- return fmt.Sprintf("%d", t)
- }
- }
- return fmt.Sprintf("%v", pdu.Value)
-}
diff --git a/nexus-scanner/internal/modules/sysarp/sysarp.go b/nexus-scanner/internal/modules/sysarp/sysarp.go
deleted file mode 100644
index 6488c4b..0000000
--- a/nexus-scanner/internal/modules/sysarp/sysarp.go
+++ /dev/null
@@ -1,142 +0,0 @@
-// Package sysarp implements a cross-platform ARP-table reader.
-// It runs "arp -a" (available on Windows, Linux and macOS) and
-// writes every discovered host into the shared SQLite store.
-// Unlike the arp_discovery module it does NOT send packets —
-// it reads what the OS already knows, so no raw-socket privilege is needed.
-package sysarp
-
-import (
- "bufio"
- "bytes"
- "context"
- "fmt"
- "log/slog"
- "os/exec"
- "regexp"
- "runtime"
- "strings"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/db"
-)
-
-// Module reads the OS ARP cache and persists discovered hosts.
-type Module struct {
- site string
- store *db.Store
- log *slog.Logger
-}
-
-// New returns a new sys_arp module.
-func New(site string, store *db.Store) *Module {
- return &Module{
- site: site,
- store: store,
- log: slog.With("module", "sys_arp"),
- }
-}
-
-func (m *Module) Name() string { return "sys_arp" }
-func (m *Module) Interval() time.Duration { return 2 * time.Minute }
-
-// Run executes one read of the ARP cache.
-func (m *Module) Run(ctx context.Context) error {
- scanID, err := m.store.BeginScan(m.Name())
- if err != nil {
- return fmt.Errorf("begin scan: %w", err)
- }
-
- scanErr := m.scan(ctx)
- if endErr := m.store.EndScan(scanID, scanErr); endErr != nil {
- m.log.Error("record scan end", "err", endErr)
- }
- return scanErr
-}
-
-func (m *Module) scan(ctx context.Context) error {
- out, err := exec.CommandContext(ctx, "arp", "-a").Output()
- if err != nil {
- return fmt.Errorf("arp -a failed: %w", err)
- }
-
- entries := parseARPOutput(out)
- m.log.Info("ARP cache read", "entries", len(entries))
-
- now := time.Now().UTC()
- saved := 0
- for _, e := range entries {
- if err := m.store.UpsertHost(db.Host{
- IP: e.ip,
- MAC: e.mac,
- Site: m.site,
- FirstSeen: now,
- LastSeen: now,
- }); err != nil {
- m.log.Error("upsert host", "ip", e.ip, "err", err)
- continue
- }
- m.log.Info("host found", "ip", e.ip, "mac", e.mac)
- saved++
- }
-
- m.log.Info("scan complete", "found", len(entries), "saved", saved)
- return nil
-}
-
-type arpEntry struct{ ip, mac string }
-
-// Windows: " 192.168.0.1 aa-bb-cc-dd-ee-ff dynamic"
-var reWindows = regexp.MustCompile(`^\s+([\d.]+)\s+([0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5})\s+\S+`)
-
-// Linux/macOS: "? (192.168.0.1) at aa:bb:cc:dd:ee:ff [ether] on eth0"
-var reUnix = regexp.MustCompile(`\(([\d.]+)\) at ([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})`)
-
-// skipPrefixes are IPs that are multicast / broadcast — not real hosts.
-var skipPrefixes = []string{"224.", "239.", "255.", "ff02"}
-
-func parseARPOutput(data []byte) []arpEntry {
- var entries []arpEntry
- seen := make(map[string]struct{})
-
- scanner := bufio.NewScanner(bytes.NewReader(data))
- for scanner.Scan() {
- line := scanner.Text()
- var ip, mac string
-
- if runtime.GOOS == "windows" {
- m := reWindows.FindStringSubmatch(line)
- if m == nil {
- continue
- }
- ip = m[1]
- // Normalise Windows dashes to colons: aa-bb-cc → aa:bb:cc
- mac = strings.ReplaceAll(strings.ToLower(m[2]), "-", ":")
- } else {
- m := reUnix.FindStringSubmatch(line)
- if m == nil {
- continue
- }
- ip = m[1]
- mac = strings.ToLower(m[2])
- }
-
- // Skip multicast, broadcast and already-seen IPs.
- skip := false
- for _, pfx := range skipPrefixes {
- if strings.HasPrefix(ip, pfx) || strings.HasPrefix(mac, pfx) {
- skip = true
- break
- }
- }
- if skip {
- continue
- }
- if _, dup := seen[ip]; dup {
- continue
- }
- seen[ip] = struct{}{}
- entries = append(entries, arpEntry{ip: ip, mac: mac})
- }
-
- return entries
-}
diff --git a/nexus-scanner/internal/scheduler/scheduler.go b/nexus-scanner/internal/scheduler/scheduler.go
deleted file mode 100644
index e95bd10..0000000
--- a/nexus-scanner/internal/scheduler/scheduler.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package scheduler
-
-import (
- "context"
- "log/slog"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/module"
-)
-
-// Scheduler runs registered modules at their configured intervals.
-type Scheduler struct {
- registry *module.Registry
-}
-
-// New returns a new Scheduler backed by the given registry.
-func New(registry *module.Registry) *Scheduler {
- return &Scheduler{registry: registry}
-}
-
-// Run starts all modules in background goroutines and blocks until ctx is cancelled.
-func (s *Scheduler) Run(ctx context.Context) {
- for _, m := range s.registry.All() {
- go s.runModule(ctx, m)
- }
- <-ctx.Done()
-}
-
-func (s *Scheduler) runModule(ctx context.Context, m module.Module) {
- log := slog.With("module", m.Name())
-
- // Run immediately on startup.
- s.execute(ctx, m, log)
-
- // If interval is zero, run once and stop.
- if m.Interval() == 0 {
- return
- }
-
- ticker := time.NewTicker(m.Interval())
- defer ticker.Stop()
-
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- s.execute(ctx, m, log)
- }
- }
-}
-
-func (s *Scheduler) execute(ctx context.Context, m module.Module, log *slog.Logger) {
- log.Info("run started")
- start := time.Now()
-
- if err := m.Run(ctx); err != nil {
- log.Error("run failed", "err", err, "elapsed", time.Since(start).Round(time.Millisecond))
- return
- }
-
- log.Info("run completed", "elapsed", time.Since(start).Round(time.Millisecond))
-}
diff --git a/nexus-scanner/internal/web/embed.go b/nexus-scanner/internal/web/embed.go
deleted file mode 100644
index 0020a62..0000000
--- a/nexus-scanner/internal/web/embed.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package web
-
-import "embed"
-
-//go:embed static templates
-var webFS embed.FS
diff --git a/nexus-scanner/internal/web/server.go b/nexus-scanner/internal/web/server.go
deleted file mode 100644
index d8fb90b..0000000
--- a/nexus-scanner/internal/web/server.go
+++ /dev/null
@@ -1,1294 +0,0 @@
-package web
-
-import (
- "context"
- "crypto/rand"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "html/template"
- "log/slog"
- "net"
- "net/http"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/cereda-systems/nexus-scanner/internal/config"
- "github.com/cereda-systems/nexus-scanner/internal/db"
- "github.com/cereda-systems/nexus-scanner/internal/module"
-)
-
-// LogEntry is a single captured log line shown on the /logs page.
-type LogEntry struct {
- Time string
- Level string
- LevelClass string
- Message string
-}
-
-// LogBuffer is a fixed-size in-memory ring of log entries.
-type LogBuffer struct {
- mu sync.Mutex
- entries []LogEntry
- max int
-}
-
-// NewLogBuffer creates a log ring buffer with capacity max.
-func NewLogBuffer(max int) *LogBuffer { return &LogBuffer{max: max} }
-
-func (b *LogBuffer) Add(e LogEntry) {
- b.mu.Lock()
- defer b.mu.Unlock()
- b.entries = append(b.entries, e)
- if len(b.entries) > b.max {
- b.entries = b.entries[len(b.entries)-b.max:]
- }
-}
-
-func (b *LogBuffer) All(level string) []LogEntry {
- b.mu.Lock()
- defer b.mu.Unlock()
- if level == "" {
- out := make([]LogEntry, len(b.entries))
- copy(out, b.entries)
- return out
- }
- var out []LogEntry
- for _, e := range b.entries {
- if e.Level == level {
- out = append(out, e)
- }
- }
- return out
-}
-
-func (b *LogBuffer) Clear() {
- b.mu.Lock()
- defer b.mu.Unlock()
- b.entries = b.entries[:0]
-}
-
-// NewSlogHandler wraps an existing handler and mirrors records into buf.
-func NewSlogHandler(inner slog.Handler, buf *LogBuffer) *SlogHandler {
- return &SlogHandler{inner: inner, buffer: buf}
-}
-
-// SlogHandler captures log records into a LogBuffer.
-type SlogHandler struct {
- inner slog.Handler
- buffer *LogBuffer
-}
-
-func (h *SlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
- return h.inner.Enabled(ctx, level)
-}
-func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
- return &SlogHandler{inner: h.inner.WithAttrs(attrs), buffer: h.buffer}
-}
-func (h *SlogHandler) WithGroup(name string) slog.Handler {
- return &SlogHandler{inner: h.inner.WithGroup(name), buffer: h.buffer}
-}
-func (h *SlogHandler) Handle(ctx context.Context, r slog.Record) error {
- lvl := r.Level.String()
- cls := strings.ToLower(lvl)
- if cls == "debug" {
- cls = "ok"
- }
- h.buffer.Add(LogEntry{
- Time: r.Time.Format("15:04:05"),
- Level: lvl,
- LevelClass: cls,
- Message: r.Message,
- })
- return h.inner.Handle(ctx, r)
-}
-
-// sessionStore is a simple in-memory session store.
-type sessionStore struct {
- mu sync.Mutex
- sessions map[string]time.Time
-}
-
-func (s *sessionStore) Create() string {
- b := make([]byte, 16)
- rand.Read(b)
- id := hex.EncodeToString(b)
- s.mu.Lock()
- s.sessions[id] = time.Now().Add(24 * time.Hour)
- s.mu.Unlock()
- return id
-}
-
-func (s *sessionStore) Valid(id string) bool {
- s.mu.Lock()
- exp, ok := s.sessions[id]
- s.mu.Unlock()
- return ok && time.Now().Before(exp)
-}
-
-func (s *sessionStore) Delete(id string) {
- s.mu.Lock()
- delete(s.sessions, id)
- s.mu.Unlock()
-}
-
-// ModuleInfo is used in templates.
-type ModuleInfo struct {
- Name string
- DisplayName string
- Description string
- Interval string
- Enabled bool
- LastRun string
-}
-
-// HostView extends db.Host with formatted fields for templates.
-type HostView struct {
- db.Host
- LastSeenFmt string
- OpenPorts []int
-}
-
-// Server is the embedded HTTP server.
-type Server struct {
- addr string
- cfgPath string // path to config.yaml on disk (for saving after setup)
- cfg *config.Config
- store *db.Store
- registry *module.Registry
- sessions *sessionStore
- logBuf *LogBuffer
- tmpl *template.Template
- mux *http.ServeMux
-
- scanMu sync.Mutex
- scanning bool
-}
-
-// NewServer creates and wires up the server.
-func NewServer(cfgPath string, cfg *config.Config, store *db.Store, registry *module.Registry, logBuf *LogBuffer) *Server {
- s := &Server{
- addr: cfg.Web.Addr,
- cfgPath: cfgPath,
- cfg: cfg,
- store: store,
- registry: registry,
- sessions: &sessionStore{sessions: make(map[string]time.Time)},
- logBuf: logBuf,
- mux: http.NewServeMux(),
- }
-
- s.loadTemplates()
- s.routes()
- return s
-}
-
-// LogBuffer returns the server's log buffer so main.go can wire slog into it.
-func (s *Server) LogBuffer() *LogBuffer { return s.logBuf }
-
-func (s *Server) loadTemplates() {
- funcMap := template.FuncMap{
- "inc": func(i int) int { return i + 1 },
- "dec": func(i int) int { return i - 1 },
- "mul": func(a, b int) int { return a * b },
- "formatTime": func(t time.Time) string { return t.Format("02.01.2006 15:04") },
- }
- t := template.New("").Funcs(funcMap)
-
- for _, name := range []string{
- "templates/layout.html",
- "templates/login.html",
- "templates/setup.html",
- "templates/dashboard.html",
- "templates/assets.html",
- "templates/modules.html",
- "templates/logs.html",
- "templates/settings.html",
- "templates/monitoring.html",
- "templates/snmp.html",
- } {
- template.Must(t.ParseFS(webFS, name))
- }
-
- s.tmpl = t
-}
-
-func (s *Server) routes() {
- s.mux.Handle("GET /static/", http.FileServer(http.FS(webFS)))
- s.mux.HandleFunc("GET /healthz", s.handleHealthz)
-
- // Setup wizard — only accessible before setup_complete=true
- s.mux.HandleFunc("GET /setup", s.setupGuard(s.handleSetupForm))
- s.mux.HandleFunc("POST /setup", s.setupGuard(s.handleSetupPost))
- s.mux.HandleFunc("POST /setup/complete", s.setupGuard(s.handleSetupComplete))
-
- // Login (only accessible after setup)
- s.mux.HandleFunc("GET /login", s.requireSetup(s.handleLoginForm))
- s.mux.HandleFunc("POST /login", s.requireSetup(s.handleLogin))
- s.mux.HandleFunc("GET /logout", s.handleLogout)
-
- // Protected app pages
- s.mux.HandleFunc("GET /", s.requireSetup(s.auth(s.handleRoot)))
- s.mux.HandleFunc("GET /dashboard", s.requireSetup(s.auth(s.handleDashboard)))
- s.mux.HandleFunc("GET /assets", s.requireSetup(s.auth(s.handleAssets)))
- s.mux.HandleFunc("GET /assets/export.csv", s.requireSetup(s.auth(s.handleAssetsCSV)))
- s.mux.HandleFunc("GET /modules", s.requireSetup(s.auth(s.handleModules)))
- s.mux.HandleFunc("POST /modules/{name}/toggle", s.requireSetup(s.auth(s.handleModuleToggle)))
- s.mux.HandleFunc("POST /modules/arp_discovery/config", s.requireSetup(s.auth(s.handleARPConfig)))
- s.mux.HandleFunc("GET /snmp", s.requireSetup(s.auth(s.handleSNMP)))
- s.mux.HandleFunc("POST /snmp/targets/add", s.requireSetup(s.auth(s.handleSNMPAddTarget)))
- s.mux.HandleFunc("POST /snmp/targets/{id}/delete", s.requireSetup(s.auth(s.handleSNMPDeleteTarget)))
- s.mux.HandleFunc("GET /monitoring", s.requireSetup(s.auth(s.handleMonitoring)))
- s.mux.HandleFunc("POST /monitoring/checks/add", s.requireSetup(s.auth(s.handleMonitoringAddCheck)))
- s.mux.HandleFunc("POST /monitoring/checks/{id}/delete", s.requireSetup(s.auth(s.handleMonitoringDeleteCheck)))
- s.mux.HandleFunc("POST /monitoring/scan", s.requireSetup(s.auth(s.handleMonitoringScan)))
- s.mux.HandleFunc("GET /ad", s.requireSetup(s.auth(s.handleAD)))
- s.mux.HandleFunc("GET /logs", s.requireSetup(s.auth(s.handleLogs)))
- s.mux.HandleFunc("GET /logs/rows", s.requireSetup(s.auth(s.handleLogsRows)))
- s.mux.HandleFunc("POST /logs/clear", s.requireSetup(s.auth(s.handleLogsClear)))
- s.mux.HandleFunc("GET /settings", s.requireSetup(s.auth(s.handleSettings)))
- s.mux.HandleFunc("POST /settings", s.requireSetup(s.auth(s.handleSettingsSave)))
- s.mux.HandleFunc("POST /scan/start", s.requireSetup(s.auth(s.handleScanStart)))
-}
-
-// requireSetup redirects to /setup if first-run wizard hasn't been completed.
-func (s *Server) requireSetup(next http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if !s.cfg.SetupComplete {
- http.Redirect(w, r, "/setup", http.StatusSeeOther)
- return
- }
- next(w, r)
- }
-}
-
-// setupGuard redirects to /dashboard if setup is already done.
-func (s *Server) setupGuard(next http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if s.cfg.SetupComplete {
- http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
- return
- }
- next(w, r)
- }
-}
-
-// ListenAndServe starts the HTTP server and blocks until ctx is cancelled.
-func (s *Server) ListenAndServe(ctx context.Context) error {
- srv := &http.Server{
- Addr: s.addr,
- Handler: s.mux,
- ReadTimeout: 10 * time.Second,
- WriteTimeout: 30 * time.Second,
- IdleTimeout: 60 * time.Second,
- }
- go func() {
- <-ctx.Done()
- shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- srv.Shutdown(shutCtx)
- }()
- slog.Info("web server listening", "addr", s.addr)
- if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- return fmt.Errorf("web: %w", err)
- }
- return nil
-}
-
-/* ── Auth ─────────────────────────────────────────────────────────── */
-
-func (s *Server) auth(next http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- cookie, err := r.Cookie("nx_session")
- if err != nil || !s.sessions.Valid(cookie.Value) {
- http.Redirect(w, r, "/login", http.StatusSeeOther)
- return
- }
- next(w, r)
- }
-}
-
-func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
- // Already logged in?
- if cookie, err := r.Cookie("nx_session"); err == nil && s.sessions.Valid(cookie.Value) {
- http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
- return
- }
- s.renderLogin(w, "")
-}
-
-func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
- pw := r.FormValue("password")
- if pw == "" || pw != s.cfg.Web.Token {
- s.renderLogin(w, "Falsches Passwort.")
- return
- }
- id := s.sessions.Create()
- http.SetCookie(w, &http.Cookie{
- Name: "nx_session", Value: id,
- Path: "/", MaxAge: 86400, HttpOnly: true, SameSite: http.SameSiteLaxMode,
- })
- http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
-}
-
-func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
- if cookie, err := r.Cookie("nx_session"); err == nil {
- s.sessions.Delete(cookie.Value)
- }
- http.SetCookie(w, &http.Cookie{Name: "nx_session", Path: "/", MaxAge: -1})
- http.Redirect(w, r, "/login", http.StatusSeeOther)
-}
-
-func (s *Server) renderLogin(w http.ResponseWriter, errMsg string) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- s.tmpl.ExecuteTemplate(w, "login", map[string]any{
- "Error": errMsg,
- })
-}
-
-/* ── Setup ────────────────────────────────────────────────────────── */
-
-func (s *Server) handleSetupForm(w http.ResponseWriter, r *http.Request) {
- stepStr := r.URL.Query().Get("step")
- step, _ := strconv.Atoi(stepStr)
- s.renderSetup(w, step, nil, nil)
-}
-
-func (s *Server) handleSetupPost(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
- step, _ := strconv.Atoi(r.FormValue("step"))
- next, _ := strconv.Atoi(r.FormValue("next"))
-
- switch step {
- case 1: // validate subnet
- subnet := r.FormValue("subnet")
- if !strings.Contains(subnet, "/") {
- s.renderSetup(w, step, fmt.Errorf("Ungültige CIDR-Notation (Beispiel: 192.168.0.0/24)"), r.Form)
- return
- }
- case 3: // validate password
- pw := r.FormValue("password")
- confirm := r.FormValue("confirm")
- if len(pw) < 6 {
- s.renderSetup(w, step, fmt.Errorf("Passwort muss mindestens 6 Zeichen haben"), r.Form)
- return
- }
- if pw != confirm {
- s.renderSetup(w, step, fmt.Errorf("Passwörter stimmen nicht überein"), r.Form)
- return
- }
- }
-
- s.renderSetup(w, next, nil, r.Form)
-}
-
-func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- // Apply wizard values to the in-memory config.
- if site := r.FormValue("site"); site != "" {
- s.cfg.Site = site
- }
- if subnet := r.FormValue("subnet"); subnet != "" {
- s.cfg.Modules.ARPDiscovery.Subnets = []string{subnet}
- s.cfg.Modules.ARPDiscovery.Enabled = true
- }
- if iface := r.FormValue("interface"); iface != "" {
- s.cfg.Modules.ARPDiscovery.Interface = iface
- }
- if url := r.FormValue("nexus_url"); url != "" {
- s.cfg.Nexus.URL = url
- }
- if key := r.FormValue("nexus_api_key"); key != "" {
- s.cfg.Nexus.APIKey = key
- }
- if pw := r.FormValue("password"); len(pw) >= 6 {
- s.cfg.Web.Token = pw
- }
-
- s.cfg.SetupComplete = true
-
- // Persist to disk so the wizard doesn't re-appear after restart.
- if err := config.Save(s.cfgPath, s.cfg); err != nil {
- slog.Error("save config after setup", "err", err)
- // Don't block the user — continue anyway.
- } else {
- slog.Info("config saved after setup", "path", s.cfgPath, "site", s.cfg.Site)
- }
-
- http.Redirect(w, r, "/login", http.StatusSeeOther)
-}
-
-func (s *Server) renderSetup(w http.ResponseWriter, step int, err error, form map[string][]string) {
- steps := []int{0, 1, 2, 3, 4}
-
- // Detect available network interfaces (up, non-loopback, with IP).
- ifaces := detectInterfaces()
- selectedIface := getForm(form, "interface")
- if selectedIface == "" && len(ifaces) > 0 {
- selectedIface = ifaces[0]
- }
-
- data := map[string]any{
- "Step": step,
- "Steps": steps,
- "Error": func() string {
- if err != nil {
- return err.Error()
- }
- return ""
- }(),
- "Site": getForm(form, "site"),
- "Subnet": getForm(form, "subnet"),
- "Interface": selectedIface,
- "Interfaces": ifaces,
- "NexusURL": getForm(form, "nexus_url"),
- "NexusAPIKey": getForm(form, "nexus_api_key"),
- "Password": getForm(form, "password"),
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- s.tmpl.ExecuteTemplate(w, "setup", data)
-}
-
-/* ── Pages ─────────────────────────────────────────────────────────── */
-
-func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
-}
-
-func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
- count, _ := s.store.CountHosts()
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "status": "ok", "hosts": count, "time": time.Now().UTC(),
- })
-}
-
-func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
- hosts, _ := s.store.ListHosts("")
- count, _ := s.store.CountHosts()
- online, offline := 0, 0
- for _, h := range hosts {
- if h.Status == "online" {
- online++
- } else {
- offline++
- }
- }
- recent := hosts
- if len(recent) > 5 {
- recent = recent[:5]
- }
- recentViews := make([]HostView, len(recent))
- for i, h := range recent {
- recentViews[i] = HostView{Host: h, LastSeenFmt: formatAgo(h.LastSeen)}
- }
-
- lastScan, _ := s.store.LastScanTime("sys_arp")
- lastScanStr := "—"
- if !lastScan.IsZero() {
- lastScanStr = formatAgo(lastScan)
- }
-
- // Count hosts added in last hour as "recent"
- recentCount := 0
- for _, h := range hosts {
- if time.Since(h.FirstSeen) < time.Hour {
- recentCount++
- }
- }
-
- s.scanMu.Lock()
- scanning := s.scanning
- s.scanMu.Unlock()
-
- s.renderPage(w, "dashboard", map[string]any{
- "Page": "dashboard",
- "Title": "Übersicht",
- "Scanning": scanning,
- "HostCount": count,
- "OnlineCount": online,
- "OfflineCount": offline,
- "RecentCount": recentCount,
- "LastScan": lastScanStr,
- "ARPInterval": s.cfg.Modules.ARPDiscovery.Interval.String(),
- "RecentHosts": recentViews,
- "Modules": s.moduleInfos(),
- })
-}
-
-func (s *Server) handleAssets(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query().Get("q")
- siteF := r.URL.Query().Get("site")
- statusF := r.URL.Query().Get("status")
- pageStr := r.URL.Query().Get("page")
- page, _ := strconv.Atoi(pageStr)
- if page < 1 {
- page = 1
- }
- const perPage = 20
-
- all, _ := s.store.ListHosts(siteF)
-
- // Filter
- var filtered []db.Host
- for _, h := range all {
- if q != "" {
- lower := strings.ToLower(q)
- if !strings.Contains(strings.ToLower(h.IP), lower) &&
- !strings.Contains(strings.ToLower(h.MAC), lower) &&
- !strings.Contains(strings.ToLower(h.Hostname), lower) {
- continue
- }
- }
- if statusF != "" && h.Status != statusF {
- continue
- }
- filtered = append(filtered, h)
- }
-
- total := len(filtered)
- totalPages := (total + perPage - 1) / perPage
- start := (page - 1) * perPage
- end := start + perPage
- if end > total {
- end = total
- }
- var pageHosts []db.Host
- if start < total {
- pageHosts = filtered[start:end]
- }
-
- allPorts, _ := s.store.ListAllHostPorts()
- views := make([]HostView, len(pageHosts))
- for i, h := range pageHosts {
- views[i] = HostView{Host: h, LastSeenFmt: formatAgo(h.LastSeen), OpenPorts: allPorts[h.IP]}
- }
-
- pages := make([]int, totalPages)
- for i := range pages {
- pages[i] = i + 1
- }
-
- qs := fmt.Sprintf("q=%s&site=%s&status=%s", q, siteF, statusF)
-
- lastScan, _ := s.store.LastScanTime("sys_arp")
- lastScanStr := "gerade eben"
- if !lastScan.IsZero() {
- lastScanStr = formatAgo(lastScan)
- }
-
- s.renderPage(w, "assets", map[string]any{
- "Page": "assets",
- "Title": "Assets",
- "Hosts": views,
- "TotalHosts": total,
- "LastScan": lastScanStr,
- "SiteFilter": siteF,
- "StatusFilter": statusF,
- "Query": q,
- "PageNum": page,
- "TotalPages": totalPages,
- "Pages": pages,
- "QueryString": qs,
- })
-}
-
-func (s *Server) handleAssetsCSV(w http.ResponseWriter, r *http.Request) {
- hosts, _ := s.store.ListHosts("")
- w.Header().Set("Content-Type", "text/csv; charset=utf-8")
- w.Header().Set("Content-Disposition", `attachment; filename="assets.csv"`)
- fmt.Fprintln(w, "IP,MAC,Hersteller,Hostname,Standort,Zuletzt gesehen,Status")
- for _, h := range hosts {
- fmt.Fprintf(w, "%s,%s,%s,%s,%s,%s,%s\n",
- h.IP, h.MAC, h.Vendor, h.Hostname, h.Site,
- h.LastSeen.Format("2006-01-02 15:04:05"), h.Status)
- }
-}
-
-type ADComputerView struct {
- db.ADComputer
- SeenInNetwork bool
-}
-
-func (s *Server) handleAD(w http.ResponseWriter, r *http.Request) {
- computers, _ := s.store.ListADComputers()
- hosts, _ := s.store.ListHosts("")
-
- // Build hostname set for quick lookup.
- knownHostnames := make(map[string]struct{})
- for _, h := range hosts {
- if h.Hostname != "" {
- knownHostnames[strings.ToLower(h.Hostname)] = struct{}{}
- }
- }
-
- views := make([]ADComputerView, len(computers))
- seen, missing := 0, 0
- for i, c := range computers {
- _, inNet := knownHostnames[strings.ToLower(c.CN)]
- views[i] = ADComputerView{ADComputer: c, SeenInNetwork: inNet}
- if inNet {
- seen++
- } else {
- missing++
- }
- }
-
- funcMap := template.FuncMap{
- "inc": func(i int) int { return i + 1 },
- "dec": func(i int) int { return i - 1 },
- "formatTime": func(t time.Time) string { return t.Format("02.01.2006 15:04") },
- "not": func(b bool) bool { return !b },
- }
- t, err := template.New("").Funcs(funcMap).ParseFS(webFS,
- "templates/layout.html",
- "templates/ad.html",
- )
- if err != nil {
- http.Error(w, "template error", 500)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- t.ExecuteTemplate(w, "layout", map[string]any{
- "Page": "ad",
- "Title": "Active Directory",
- "SiteID": s.cfg.Site,
- "Addr": s.cfg.Web.Addr,
- "Computers": views,
- "Domain": s.cfg.Modules.ADSync.SearchBase,
- "ADEnabled": s.cfg.Modules.ADSync.Enabled,
- "SeenCount": seen,
- "MissingCount": missing,
- })
-}
-
-func (s *Server) handleModules(w http.ResponseWriter, r *http.Request) {
- s.renderPage(w, "modules", map[string]any{
- "Page": "modules",
- "Title": "Module",
- "Modules": s.moduleInfos(),
- "ARPSubnets": s.cfg.Modules.ARPDiscovery.Subnets,
- "ARPInterface": s.cfg.Modules.ARPDiscovery.Interface,
- })
-}
-
-func (s *Server) handleModuleToggle(w http.ResponseWriter, r *http.Request) {
- name := r.PathValue("name")
- current := s.store.GetModuleEnabled(name)
- enabled := !current
- if err := s.store.SetModuleEnabled(name, enabled); err != nil {
- slog.Error("toggle module", "module", name, "err", err)
- } else {
- slog.Info("module toggled", "module", name, "enabled", enabled)
- }
- http.Redirect(w, r, "/modules", http.StatusSeeOther)
-}
-
-func (s *Server) handleARPConfig(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
- subnets := r.Form["subnet"]
- iface := r.FormValue("interface")
- if len(subnets) > 0 {
- s.cfg.Modules.ARPDiscovery.Subnets = subnets
- }
- if iface != "" {
- s.cfg.Modules.ARPDiscovery.Interface = iface
- }
- if err := config.Save(s.cfgPath, s.cfg); err != nil {
- slog.Error("save arp config", "err", err)
- } else {
- slog.Info("arp config saved", "subnets", subnets, "interface", iface)
- }
- http.Redirect(w, r, "/modules", http.StatusSeeOther)
-}
-
-func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
- level := r.URL.Query().Get("level")
- logs := s.logBuf.All(level)
- reverseLogs(logs)
- s.renderPage(w, "logs", map[string]any{
- "Page": "logs",
- "Title": "Logs",
- "Logs": logs,
- "LevelFilter": level,
- })
-}
-
-func (s *Server) handleLogsRows(w http.ResponseWriter, r *http.Request) {
- level := r.URL.Query().Get("level")
- logs := s.logBuf.All(level)
- reverseLogs(logs)
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- s.tmpl.ExecuteTemplate(w, "log-rows", map[string]any{
- "Logs": logs,
- "LevelFilter": level,
- })
-}
-
-func (s *Server) handleLogsClear(w http.ResponseWriter, r *http.Request) {
- s.logBuf.Clear()
- w.WriteHeader(http.StatusNoContent)
-}
-
-func (s *Server) settingsData(extra map[string]any) map[string]any {
- data := map[string]any{
- "Page": "settings",
- "Title": "Einstellungen",
- "Site": s.cfg.Site,
- "NexusURL": s.cfg.Nexus.URL,
- "NexusAPIKey": s.cfg.Nexus.APIKey,
- "ARPInterval": s.cfg.Modules.ARPDiscovery.Interval.String(),
- "ADEnabled": s.cfg.Modules.ADSync.Enabled,
- "ADServer": s.cfg.Modules.ADSync.Server,
- "ADPort": s.cfg.Modules.ADSync.Port,
- "ADBindDN": s.cfg.Modules.ADSync.BindDN,
- "ADBindPW": s.cfg.Modules.ADSync.BindPassword,
- "ADSearchBase": s.cfg.Modules.ADSync.SearchBase,
- "ADInterval": s.cfg.Modules.ADSync.Interval.String(),
- "OfflineAfter": s.cfg.OfflineAfter.String(),
- "AlertNexus": s.cfg.Alert.NexusEnabled,
- "SMTPEnabled": s.cfg.Alert.SMTP.Enabled,
- "SMTPHost": s.cfg.Alert.SMTP.Host,
- "SMTPPort": s.cfg.Alert.SMTP.Port,
- "SMTPUser": s.cfg.Alert.SMTP.Username,
- "SMTPPass": s.cfg.Alert.SMTP.Password,
- "SMTPFrom": s.cfg.Alert.SMTP.From,
- "SMTPTo": s.cfg.Alert.SMTP.To,
- }
- for k, v := range extra {
- data[k] = v
- }
- return data
-}
-
-func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
- s.renderPage(w, "settings", s.settingsData(nil))
-}
-
-func (s *Server) handleSettingsSave(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- // Apply general settings.
- if site := r.FormValue("site"); site != "" {
- s.cfg.Site = site
- }
- s.cfg.Nexus.URL = r.FormValue("nexus_url")
- if key := r.FormValue("nexus_api_key"); key != "" {
- s.cfg.Nexus.APIKey = key
- }
-
- // Apply AD settings.
- s.cfg.Modules.ADSync.Enabled = r.FormValue("ad_enabled") == "on"
- s.cfg.Modules.ADSync.Server = r.FormValue("ad_server")
- s.cfg.Modules.ADSync.BindDN = r.FormValue("ad_bind_dn")
- if pw := r.FormValue("ad_bind_password"); pw != "" {
- s.cfg.Modules.ADSync.BindPassword = pw
- }
- if sb := r.FormValue("ad_search_base"); sb != "" {
- s.cfg.Modules.ADSync.SearchBase = sb
- }
- if port := r.FormValue("ad_port"); port != "" {
- if p, err := strconv.Atoi(port); err == nil && p > 0 {
- s.cfg.Modules.ADSync.Port = p
- }
- }
- if iv := r.FormValue("ad_interval"); iv != "" {
- if d, err := time.ParseDuration(iv); err == nil {
- s.cfg.Modules.ADSync.Interval = d
- }
- }
-
- // offline_after
- if oa := r.FormValue("offline_after"); oa != "" {
- if d, err := time.ParseDuration(oa); err == nil && d > 0 {
- s.cfg.OfflineAfter = d
- }
- }
-
- // Apply alert settings.
- s.cfg.Alert.NexusEnabled = r.FormValue("alert_nexus") == "on"
- s.cfg.Alert.SMTP.Enabled = r.FormValue("smtp_enabled") == "on"
- s.cfg.Alert.SMTP.Host = r.FormValue("smtp_host")
- s.cfg.Alert.SMTP.Username = r.FormValue("smtp_user")
- if p := r.FormValue("smtp_pass"); p != "" {
- s.cfg.Alert.SMTP.Password = p
- }
- s.cfg.Alert.SMTP.From = r.FormValue("smtp_from")
- s.cfg.Alert.SMTP.To = r.FormValue("smtp_to")
- if port := r.FormValue("smtp_port"); port != "" {
- if p, err := strconv.Atoi(port); err == nil {
- s.cfg.Alert.SMTP.Port = p
- }
- }
-
- // Apply password change.
- if pw := r.FormValue("new_password"); len(pw) >= 6 {
- if pw == r.FormValue("confirm_password") {
- s.cfg.Web.Token = pw
- }
- }
-
- // Persist to disk.
- var saveErr string
- if err := config.Save(s.cfgPath, s.cfg); err != nil {
- slog.Error("save config", "err", err)
- saveErr = err.Error()
- } else {
- slog.Info("settings saved", "site", s.cfg.Site, "ad_enabled", s.cfg.Modules.ADSync.Enabled)
- }
-
- s.renderPage(w, "settings", s.settingsData(map[string]any{
- "Saved": saveErr == "",
- "SaveErr": saveErr,
- }))
-}
-
-func (s *Server) handleScanStart(w http.ResponseWriter, r *http.Request) {
- s.scanMu.Lock()
- if !s.scanning {
- s.scanning = true
- go func() {
- slog.Info("manual scan triggered")
- ctx := context.Background()
- for _, m := range s.registry.All() {
- if err := m.Run(ctx); err != nil {
- slog.Error("manual scan error", "module", m.Name(), "err", err)
- }
- }
- s.scanMu.Lock()
- s.scanning = false
- s.scanMu.Unlock()
- slog.Info("manual scan completed")
- }()
- }
- s.scanMu.Unlock()
- http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
-}
-
-/* ── SNMP ──────────────────────────────────────────────────────────── */
-
-type SNMPTargetView struct {
- Target db.SNMPTarget
- HasData bool
- SysName string
- SysDescr string
- SysUpTime string
- SysLocation string
- SysContact string
- TonerLevel string
- TonerMax string
- TonerPct int
- PageCount string
- PrinterStatus string
- Interval string
- // Switch fields
- PortsTotal string
- PortsUp string
- TrafficIn string
- TrafficOut string
- MemUsed string
- MemTotal string
- MemPct int
- CpuLoad string
- IfNumber string
- // Raw OID map for detail modal
- AllValues map[string]string
-}
-
-func (s *Server) handleSNMP(w http.ResponseWriter, r *http.Request) {
- targets, _ := s.store.ListSNMPTargets()
- results, _ := s.store.LatestSNMPResults()
-
- var views []SNMPTargetView
- for _, t := range targets {
- v := SNMPTargetView{Target: t, Interval: s.cfg.Modules.SNMP.Interval.String()}
- if vals, ok := results[t.ID]; ok && len(vals) > 0 {
- v.HasData = true
- v.AllValues = vals
- v.SysName = vals["sysName"]
- v.SysDescr = vals["sysDescr"]
- v.SysUpTime = vals["sysUpTime"]
- v.SysLocation = vals["sysLocation"]
- v.SysContact = vals["sysContact"]
- v.TonerLevel = vals["tonerLevel"]
- v.TonerMax = vals["tonerMax"]
- v.PageCount = vals["pageCount"]
- switch vals["printerStatus"] {
- case "3":
- v.PrinterStatus = "Bereit"
- case "4":
- v.PrinterStatus = "Druckt"
- case "5":
- v.PrinterStatus = "Aufwärmen"
- default:
- v.PrinterStatus = ""
- }
- v.PortsTotal = vals["portsTotal"]
- v.PortsUp = vals["portsUp"]
- v.IfNumber = vals["ifNumber"]
- if cpu := vals["cpuLoad"]; cpu != "" && cpu != "" && !strings.Contains(cpu, "nil") {
- v.CpuLoad = cpu
- }
- if v.TonerLevel != "" {
- level, _ := strconv.Atoi(v.TonerLevel)
- if maxStr := vals["tonerMax"]; maxStr != "" {
- maxN, _ := strconv.Atoi(maxStr)
- if maxN > 0 {
- v.TonerPct = level * 100 / maxN
- }
- } else {
- v.TonerPct = level
- }
- }
- // Format traffic bytes → human readable, also update AllValues for modal
- if raw := vals["trafficIn"]; raw != "" {
- n, _ := strconv.ParseUint(raw, 10, 64)
- v.TrafficIn = formatBytes(n)
- vals["trafficIn"] = formatBytes(n)
- }
- if raw := vals["trafficOut"]; raw != "" {
- n, _ := strconv.ParseUint(raw, 10, 64)
- v.TrafficOut = formatBytes(n)
- vals["trafficOut"] = formatBytes(n)
- }
- // Memory
- if raw := vals["memUsed"]; raw != "" {
- n, _ := strconv.ParseUint(raw, 10, 64)
- v.MemUsed = formatBytes(n)
- vals["memUsed"] = formatBytes(n)
- }
- if raw := vals["memTotal"]; raw != "" {
- n, _ := strconv.ParseUint(raw, 10, 64)
- v.MemTotal = formatBytes(n)
- vals["memTotal"] = formatBytes(n)
- used, _ := strconv.ParseUint(vals["memUsed"], 10, 64)
- if n > 0 {
- v.MemPct = int(used * 100 / n)
- }
- }
- // Remove nil entries from detail modal
- for k, val := range vals {
- if val == "" || strings.Contains(val, "") {
- delete(vals, k)
- }
- }
- }
- views = append(views, v)
- }
-
- s.renderPage(w, "snmp", map[string]any{
- "Page": "snmp",
- "Title": "SNMP",
- "Targets": views,
- "AddErr": r.URL.Query().Get("err"),
- })
-}
-
-func (s *Server) handleSNMPAddTarget(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
- name := strings.TrimSpace(r.FormValue("name"))
- ip := strings.TrimSpace(r.FormValue("ip"))
- community := r.FormValue("community")
- version := r.FormValue("version")
- typ := r.FormValue("type")
- if community == "" {
- community = "public"
- }
- if name == "" || ip == "" {
- http.Redirect(w, r, "/snmp?err=Name+und+IP+erforderlich", http.StatusSeeOther)
- return
- }
- if _, err := s.store.AddSNMPTarget(name, ip, community, version, typ); err != nil {
- slog.Error("add snmp target", "err", err)
- }
- http.Redirect(w, r, "/snmp", http.StatusSeeOther)
-}
-
-func (s *Server) handleSNMPDeleteTarget(w http.ResponseWriter, r *http.Request) {
- id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
- _ = s.store.DeleteSNMPTarget(id)
- http.Redirect(w, r, "/snmp", http.StatusSeeOther)
-}
-
-/* ── Monitoring ────────────────────────────────────────────────────── */
-
-// MonitorCheckView combines a check definition with its latest result and history.
-type MonitorCheckView struct {
- Check db.MonitorCheck
- Latest db.MonitorResult
- HasResult bool
- History []db.MonitorResult
-}
-
-func (s *Server) handleMonitoring(w http.ResponseWriter, r *http.Request) {
- s.renderMonitoring(w, "")
-}
-
-func (s *Server) renderMonitoring(w http.ResponseWriter, addErr string) {
- checks, _ := s.store.ListMonitorChecks()
- latest, _ := s.store.LatestMonitorResults()
-
- var views []MonitorCheckView
- onlineCount, offlineCount := 0, 0
-
- for _, c := range checks {
- v := MonitorCheckView{Check: c}
- if r, ok := latest[c.ID]; ok {
- v.Latest = r
- v.HasResult = true
- history, _ := s.store.MonitorResultHistory(c.ID, 20)
- v.History = history
- if r.Status == "online" {
- onlineCount++
- } else {
- offlineCount++
- }
- }
- views = append(views, v)
- }
-
- allOnline := len(checks) > 0 && offlineCount == 0 && len(latest) == len(checks)
-
- s.renderPage(w, "monitoring", map[string]any{
- "Page": "monitoring",
- "Title": "Monitoring",
- "Checks": views,
- "TotalChecks": len(checks),
- "OnlineCount": onlineCount,
- "OfflineCount": offlineCount,
- "AllOnline": allOnline,
- "AddErr": addErr,
- })
-}
-
-func (s *Server) handleMonitoringAddCheck(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
- name := strings.TrimSpace(r.FormValue("name"))
- typ := r.FormValue("type")
- target := strings.TrimSpace(r.FormValue("target"))
-
- if name == "" || target == "" {
- s.renderMonitoring(w, "Name und Ziel dürfen nicht leer sein.")
- return
- }
- validTypes := map[string]bool{"ping": true, "http": true, "tcp": true}
- if !validTypes[typ] {
- s.renderMonitoring(w, "Ungültiger Typ.")
- return
- }
-
- if _, err := s.store.AddMonitorCheck(name, typ, target); err != nil {
- slog.Error("add monitor check", "err", err)
- s.renderMonitoring(w, "Fehler beim Speichern: "+err.Error())
- return
- }
- slog.Info("monitor check added", "name", name, "type", typ, "target", target)
- http.Redirect(w, r, "/monitoring", http.StatusSeeOther)
-}
-
-func (s *Server) handleMonitoringDeleteCheck(w http.ResponseWriter, r *http.Request) {
- idStr := r.PathValue("id")
- id, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- http.Error(w, "invalid id", http.StatusBadRequest)
- return
- }
- if err := s.store.DeleteMonitorCheck(id); err != nil {
- slog.Error("delete monitor check", "id", id, "err", err)
- }
- http.Redirect(w, r, "/monitoring", http.StatusSeeOther)
-}
-
-func (s *Server) handleMonitoringScan(w http.ResponseWriter, r *http.Request) {
- s.scanMu.Lock()
- if !s.scanning {
- s.scanning = true
- go func() {
- ctx := context.Background()
- for _, m := range s.registry.All() {
- if m.Name() == "site_monitoring" {
- if err := m.Run(ctx); err != nil {
- slog.Error("monitoring scan error", "err", err)
- }
- }
- }
- s.scanMu.Lock()
- s.scanning = false
- s.scanMu.Unlock()
- }()
- }
- s.scanMu.Unlock()
- http.Redirect(w, r, "/monitoring", http.StatusSeeOther)
-}
-
-/* ── Helpers ──────────────────────────────────────────────────────── */
-
-func (s *Server) renderPage(w http.ResponseWriter, page string, data map[string]any) {
- // Inject shared layout data.
- if _, ok := data["Theme"]; !ok {
- data["Theme"] = "light"
- }
- if _, ok := data["SiteID"]; !ok {
- data["SiteID"] = s.cfg.Site
- }
- if _, ok := data["HostCount"]; !ok {
- n, _ := s.store.CountHosts()
- data["HostCount"] = n
- }
- if _, ok := data["Addr"]; !ok {
- data["Addr"] = s.cfg.Web.Addr
- }
- if _, ok := data["Scanning"]; !ok {
- s.scanMu.Lock()
- data["Scanning"] = s.scanning
- s.scanMu.Unlock()
- }
-
- // Parse layout + page template together so "content" block is available.
- funcMap := template.FuncMap{
- "inc": func(i int) int { return i + 1 },
- "dec": func(i int) int { return i - 1 },
- "mul": func(a, b int) int { return a * b },
- "formatTime": func(t time.Time) string { return t.Format("02.01.2006 15:04") },
- "not": func(b bool) bool { return !b },
- }
- t, err := template.New("").Funcs(funcMap).ParseFS(webFS,
- "templates/layout.html",
- "templates/"+page+".html",
- )
- if err != nil {
- slog.Error("template parse", "page", page, "err", err)
- http.Error(w, "template error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- if err := t.ExecuteTemplate(w, "layout", data); err != nil {
- slog.Error("template exec", "page", page, "err", err)
- }
-}
-
-func (s *Server) moduleInfos() []ModuleInfo {
- var infos []ModuleInfo
- for _, m := range s.registry.All() {
- lastRun, _ := s.store.LastScanTime(m.Name())
- lastRunStr := ""
- if !lastRun.IsZero() {
- lastRunStr = formatAgo(lastRun)
- }
- infos = append(infos, ModuleInfo{
- Name: m.Name(),
- DisplayName: displayName(m.Name()),
- Description: moduleDesc(m.Name()),
- Interval: m.Interval().String(),
- Enabled: s.store.GetModuleEnabled(m.Name()),
- LastRun: lastRunStr,
- })
- }
- return infos
-}
-
-func formatAgo(t time.Time) string {
- d := time.Since(t)
- switch {
- case d < time.Minute:
- return fmt.Sprintf("vor %ds", int(d.Seconds()))
- case d < time.Hour:
- return fmt.Sprintf("vor %dm", int(d.Minutes()))
- case d < 24*time.Hour:
- return fmt.Sprintf("vor %dh", int(d.Hours()))
- default:
- return fmt.Sprintf("vor %dd", int(d.Hours()/24))
- }
-}
-
-func reverseLogs(logs []LogEntry) {
- sort.Slice(logs, func(i, j int) bool { return i > j })
-}
-
-func formatBytes(b uint64) string {
- const unit = 1024
- if b < unit {
- return fmt.Sprintf("%d B", b)
- }
- div, exp := uint64(unit), 0
- for n := b / unit; n >= unit; n /= unit {
- div *= unit
- exp++
- }
- return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
-}
-
-func getForm(form map[string][]string, key string) string {
- if form == nil {
- return ""
- }
- vals := form[key]
- if len(vals) == 0 {
- return ""
- }
- return vals[0]
-}
-
-func displayName(name string) string {
- m := map[string]string{
- "arp_discovery": "ARP-Discovery",
- "dns_reverse": "DNS-Reverse",
- "mac_vendor": "MAC-Vendor",
- "ad_sync": "Active Directory",
- "unifi_api": "UniFi Controller",
- }
- if v, ok := m[name]; ok {
- return v
- }
- return name
-}
-
-// detectInterfaces returns names of active non-loopback network interfaces.
-func detectInterfaces() []string {
- ifaces, err := net.Interfaces()
- if err != nil {
- return []string{"eth0"}
- }
- var names []string
- for _, iface := range ifaces {
- if iface.Flags&net.FlagLoopback != 0 {
- continue
- }
- if iface.Flags&net.FlagUp == 0 {
- continue
- }
- addrs, _ := iface.Addrs()
- hasIP := false
- for _, a := range addrs {
- if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
- hasIP = true
- break
- }
- }
- if hasIP {
- names = append(names, iface.Name)
- }
- }
- if len(names) == 0 {
- return []string{"eth0"}
- }
- return names
-}
-
-func moduleDesc(name string) string {
- m := map[string]string{
- "arp_discovery": "Findet alle Geräte per ARP-Broadcast",
- "dns_reverse": "Löst IPs in Hostnamen auf",
- "mac_vendor": "Bestimmt Hersteller aus MAC-Adresse",
- "ad_sync": "Synchronisiert Computer aus winkel.local",
- "unifi_api": "Liest Clients aus UniFi Controller",
- }
- if v, ok := m[name]; ok {
- return v
- }
- return ""
-}
diff --git a/nexus-scanner/internal/web/static/style.css b/nexus-scanner/internal/web/static/style.css
deleted file mode 100644
index c0356fc..0000000
--- a/nexus-scanner/internal/web/static/style.css
+++ /dev/null
@@ -1,510 +0,0 @@
-/* ── Reset ── */
-*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
-
-/* ── Design Tokens ── */
-:root {
- --teal: #008487;
- --teal-600: #006e71;
- --teal-700: #005a5c;
- --teal-50: #e6f3f3;
- --bg: #f5f5f7;
- --surface: #ffffff;
- --surface-2: #fbfbfd;
- --surface-3: #f2f2f4;
- --text: #1d1d1f;
- --text-2: #515154;
- --text-3: #86868b;
- --border: rgba(0,0,0,0.08);
- --border-strong: rgba(0,0,0,0.14);
- --shadow-sm: 0 1px 2px rgba(0,0,0,0.04), 0 1px 1px rgba(0,0,0,0.03);
- --shadow-md: 0 4px 16px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
- --shadow-lg: 0 24px 64px -16px rgba(0,0,0,0.18), 0 6px 18px rgba(0,0,0,0.08);
- --success: #34a853;
- --warning: #e8a23a;
- --danger: #e5484d;
- --info: #2f7df1;
- --r-sm: 8px;
- --r-md: 12px;
- --r-lg: 16px;
- --r-xl: 20px;
- --sidebar-w: 232px;
- --topbar-h: 52px;
-}
-
-[data-theme="dark"] {
- color-scheme: dark;
- --bg: #000000;
- --surface: #1c1c1e;
- --surface-2: #161618;
- --surface-3: #2c2c2e;
- --text: #f5f5f7;
- --text-2: #aeaeb2;
- --text-3: #636366;
- --border: rgba(255,255,255,0.08);
- --border-strong: rgba(255,255,255,0.14);
- --shadow-sm: 0 1px 2px rgba(0,0,0,0.6);
- --shadow-md: 0 6px 20px rgba(0,0,0,0.45);
- --teal-50: rgba(0,132,135,0.15);
-}
-
-/* ── Base ── */
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
- background: var(--bg);
- color: var(--text);
- font-size: 14px;
- line-height: 1.45;
- -webkit-font-smoothing: antialiased;
- letter-spacing: -0.005em;
-}
-a { text-decoration: none; color: inherit; }
-button { font-family: inherit; }
-input, select { font-family: inherit; font-size: inherit; color: inherit; }
-::selection { background: rgba(0,132,135,0.2); }
-
-/* ── Layout ── */
-.app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
-.main { display: flex; flex-direction: column; min-width: 0; }
-
-/* ── Sidebar ── */
-.sidebar {
- position: sticky; top: 0; height: 100vh;
- background: var(--surface-2);
- border-right: 1px solid var(--border);
- display: flex; flex-direction: column;
- padding: 14px 10px;
- overflow-y: auto;
-}
-.sidebar-brand {
- display: flex; align-items: center; gap: 10px;
- padding: 6px 8px 16px;
-}
-.brand-mark {
- width: 30px; height: 30px; border-radius: var(--r-sm);
- background: var(--teal); display: flex; align-items: center; justify-content: center; flex-shrink: 0;
-}
-.brand-mark svg { color: #fff; width: 17px; height: 17px; }
-.brand-name { font-size: 13.5px; font-weight: 700; color: var(--text); }
-.brand-sub { font-size: 11px; color: var(--text-3); }
-
-.sidebar-section {
- font-size: 10.5px; font-weight: 600; color: var(--text-3);
- letter-spacing: 0.05em; text-transform: uppercase;
- padding: 12px 8px 5px;
-}
-.nav-item {
- display: flex; align-items: center; gap: 9px;
- padding: 7px 8px; border-radius: var(--r-sm);
- color: var(--text-2); font-size: 13.5px; font-weight: 500;
- transition: background 0.12s, color 0.12s;
- cursor: pointer; border: none; background: none; width: 100%; text-align: left;
-}
-.nav-item:hover { background: var(--surface-3); color: var(--text); }
-.nav-item.active { background: var(--teal-50); color: var(--teal); }
-.nav-item svg { width: 17px; height: 17px; flex-shrink: 0; stroke-width: 1.75; }
-.nav-badge {
- margin-left: auto; background: var(--surface-3); color: var(--text-2);
- border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 600;
-}
-.nav-item.active .nav-badge { background: rgba(0,132,135,.18); color: var(--teal); }
-.nav-dot { margin-left: auto; width: 6px; height: 6px; border-radius: 50%; background: var(--success); }
-
-.sidebar-footer {
- margin-top: auto; padding: 10px 8px 6px;
- border-top: 1px solid var(--border);
- display: flex; align-items: center; gap: 10px;
-}
-.avatar {
- width: 27px; height: 27px; border-radius: 50%;
- background: linear-gradient(135deg, var(--teal), var(--teal-700));
- color: #fff; display: flex; align-items: center; justify-content: center;
- font-size: 11px; font-weight: 700; flex-shrink: 0;
-}
-.user-name { font-size: 12.5px; font-weight: 600; color: var(--text); }
-.user-sub { font-size: 11px; color: var(--text-3); }
-
-/* ── Topbar ── */
-.topbar {
- height: var(--topbar-h); position: sticky; top: 0;
- background: color-mix(in srgb, var(--bg) 85%, transparent);
- backdrop-filter: saturate(180%) blur(20px);
- border-bottom: 1px solid var(--border);
- display: flex; align-items: center; padding: 0 28px; gap: 8px; z-index: 50;
-}
-.breadcrumb { flex: 1; display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-3); }
-.breadcrumb-sep { color: var(--text-3); }
-.breadcrumb-current { font-weight: 600; color: var(--text); }
-.topbar-actions { display: flex; align-items: center; gap: 8px; }
-
-/* ── Content area ── */
-.content { flex: 1; padding: 24px 28px; }
-
-/* ── Page header ── */
-.page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 20px; }
-.page-header h2, .page-title { font-size: 21px; font-weight: 700; letter-spacing: -0.3px; }
-.page-header p, .page-sub { font-size: 13px; color: var(--text-3); margin-top: 2px; }
-.page-actions { display: flex; align-items: center; gap: 8px; }
-
-/* ── Scan status pill (in page header) ── */
-.scan-pill {
- display: inline-flex; align-items: center; gap: 7px;
- background: var(--surface); border: 1px solid var(--border);
- border-radius: 999px; padding: 5px 12px 5px 10px;
- font-size: 12.5px;
-}
-.scan-pill .label { font-weight: 500; color: var(--text); }
-.scan-pill .sep { color: var(--text-3); }
-.scan-pill .site { color: var(--text-3); }
-
-/* ── Status pill (topbar) ── */
-.status-pill {
- display: inline-flex; align-items: center; gap: 6px;
- padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 500;
- background: var(--surface-3); color: var(--text-2);
-}
-.status-pill.online { background: rgba(52,168,83,.1); color: var(--success); }
-.status-pill.scanning { background: rgba(0,132,135,.1); color: var(--teal); }
-
-/* ── Buttons ── */
-.btn {
- display: inline-flex; align-items: center; gap: 6px;
- padding: 7px 14px; border-radius: var(--r-sm);
- font-size: 13px; font-weight: 500;
- cursor: pointer; border: 1px solid var(--border);
- background: var(--surface); color: var(--text);
- transition: background 0.12s, border-color 0.12s; white-space: nowrap;
-}
-.btn:hover { background: var(--surface-3); }
-.btn svg { width: 14px; height: 14px; stroke-width: 1.75; }
-.btn-primary { background: var(--teal); color: #fff; border-color: var(--teal); }
-.btn-primary:hover { background: var(--teal-600); border-color: var(--teal-600); }
-.btn-danger { background: var(--danger); color: #fff; border-color: var(--danger); }
-.btn-sm { padding: 5px 11px; font-size: 12.5px; }
-.btn-ghost { background: transparent; border-color: transparent; }
-.btn-ghost:hover { background: var(--surface-3); }
-.btn:disabled { opacity: .55; cursor: not-allowed; }
-
-/* ── Icon button ── */
-.icon-btn {
- width: 30px; height: 30px; border-radius: var(--r-sm);
- border: 1px solid var(--border); background: var(--surface); color: var(--text-2);
- display: flex; align-items: center; justify-content: center;
- cursor: pointer; transition: background 0.12s;
-}
-.icon-btn:hover { background: var(--surface-3); color: var(--text); }
-.icon-btn svg { width: 14px; height: 14px; stroke-width: 1.75; }
-
-/* ── Inputs ── */
-.input, .select {
- width: 100%; padding: 7.5px 12px;
- border: 1px solid var(--border-strong); border-radius: var(--r-sm);
- background: var(--surface); color: var(--text); font-size: 13.5px;
- outline: none; transition: border-color .14s, box-shadow .14s;
-}
-.input:focus, .select:focus {
- border-color: var(--teal);
- box-shadow: 0 0 0 3px rgba(0,132,135,.14);
-}
-.input::placeholder { color: var(--text-3); }
-.input-label { font-size: 12.5px; font-weight: 500; color: var(--text-2); margin-bottom: 5px; display: block; }
-
-/* ── Toggle ── */
-.toggle { position: relative; display: inline-block; width: 36px; height: 22px; flex-shrink: 0; }
-.toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
-.toggle-track {
- position: absolute; inset: 0; background: var(--surface-3);
- border: 1px solid var(--border); border-radius: 20px;
- cursor: pointer; transition: background .18s;
-}
-.toggle input:checked + .toggle-track { background: var(--teal); border-color: var(--teal); }
-.toggle-track::after {
- content: ''; position: absolute; left: 2px; top: 2px;
- width: 16px; height: 16px; border-radius: 50%;
- background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2);
- transition: transform .18s;
-}
-.toggle input:checked + .toggle-track::after { transform: translateX(14px); }
-
-/* ── Dot ── */
-.dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
-.dot.green { background: var(--success); box-shadow: 0 0 0 2px rgba(52,168,83,.18); }
-.dot.red { background: var(--danger); }
-.dot.yellow { background: var(--warning); }
-.dot.gray { background: var(--text-3); }
-.dot.teal { background: var(--teal); }
-.dot.pulse { animation: dotpulse 1.6s ease-out infinite; }
-@keyframes dotpulse {
- 0% { box-shadow: 0 0 0 0 rgba(52,168,83,0.5); }
- 70% { box-shadow: 0 0 0 6px rgba(52,168,83,0); }
- 100% { box-shadow: 0 0 0 0 rgba(52,168,83,0); }
-}
-
-/* ── Badge ── */
-.badge {
- display: inline-flex; align-items: center; gap: 5px;
- padding: 2px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 500;
- background: var(--surface-3); color: var(--text-2);
-}
-.badge-green { background: rgba(52,168,83,.12); color: #2c8a44; }
-.badge-red { background: rgba(229,72,77,.12); color: #c4393d; }
-.badge-yellow { background: rgba(232,162,58,.14); color: #b07820; }
-.badge-teal { background: var(--teal-50); color: var(--teal); }
-.badge-gray { background: var(--surface-3); color: var(--text-3); }
-[data-theme="dark"] .badge-green { color: #4cc471; }
-[data-theme="dark"] .badge-red { color: #ff6b6f; }
-[data-theme="dark"] .badge-yellow { color: #ffc46b; }
-[data-theme="dark"] .badge-teal { color: #4ec6c9; }
-
-/* ── Card ── */
-.card {
- background: var(--surface); border: 1px solid var(--border);
- border-radius: var(--r-lg); overflow: hidden;
- box-shadow: var(--shadow-sm);
-}
-.card-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 14px 20px; border-bottom: 1px solid var(--border);
-}
-.card-title { font-size: 14px; font-weight: 600; }
-.card-sub { font-size: 12px; color: var(--text-3); margin-top: 2px; }
-.card-action { font-size: 12.5px; color: var(--teal); display: flex; align-items: center; gap: 4px; }
-.card-action svg { width: 12px; height: 12px; }
-
-/* ── Stats grid ── */
-.stats-grid { display: grid; gap: 16px; margin-bottom: 20px; }
-.stats-grid-4 { grid-template-columns: repeat(4, 1fr); }
-.stat {
- background: var(--surface); border: 1px solid var(--border);
- border-radius: var(--r-lg); padding: 18px 20px;
- box-shadow: var(--shadow-sm);
-}
-.stat-icon { width: 14px; height: 14px; color: var(--text-3); stroke-width: 1.75; }
-.stat-label { font-size: 12.5px; color: var(--text-3); font-weight: 500; margin-bottom: 6px; display: flex; align-items: center; gap: 7px; }
-.stat-value { font-size: 30px; font-weight: 600; letter-spacing: -0.02em; margin-top: 2px; }
-.stat-delta { font-size: 12.5px; color: var(--text-3); margin-top: 4px; }
-.stat-delta.up { color: var(--success); }
-.stat-delta.down { color: var(--danger); }
-
-/* ── Sparkline ── */
-.sparkline { display: flex; align-items: flex-end; gap: 2px; height: 24px; margin-top: 8px; }
-.sparkline span { flex: 1; background: var(--teal); border-radius: 1.5px; opacity: 0.75; }
-
-/* ── Dashboard 2-col grid ── */
-.dash-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
-
-/* ── Table ── */
-table { width: 100%; border-collapse: collapse; }
-thead th {
- padding: 10px 16px; text-align: left; white-space: nowrap;
- font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
- color: var(--text-3); border-bottom: 1px solid var(--border);
- background: var(--surface-2); position: sticky; top: var(--topbar-h);
-}
-tbody td { padding: 12px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
-tbody tr:last-child td { border-bottom: none; }
-tbody tr:hover td { background: var(--surface-2); }
-.mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }
-
-/* ── Device icon cell ── */
-.device-icon {
- width: 28px; height: 28px; border-radius: 7px;
- background: var(--surface-3); color: var(--text-2);
- display: flex; align-items: center; justify-content: center; flex-shrink: 0;
-}
-.device-icon svg { width: 14px; height: 14px; stroke-width: 1.75; }
-.device-row { display: flex; align-items: center; gap: 10px; }
-.device-name { font-size: 13px; font-weight: 600; }
-.device-vendor { font-size: 11.5px; color: var(--text-3); }
-
-/* ── Filter chips ── */
-.filter-bar { display: flex; align-items: center; gap: 10px; padding: 12px 16px; flex-wrap: wrap; border-bottom: 1px solid var(--border); background: var(--surface); }
-.filter-chips { display: flex; gap: 5px; }
-.chip {
- padding: 4px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 500;
- background: transparent; border: 1px solid var(--border-strong); color: var(--text-2);
- cursor: pointer; transition: all .12s;
-}
-.chip:hover { background: var(--surface-3); }
-.chip.active { background: var(--teal-50); border-color: transparent; color: var(--teal); }
-.search-wrap { position: relative; flex: 1; min-width: 200px; max-width: 300px; }
-.search-wrap svg { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--text-3); pointer-events: none; }
-.search-wrap .input { padding-left: 32px; }
-
-/* ── Module row ── */
-.module-row {
- display: flex; align-items: center; gap: 14px;
- padding: 14px 20px; border-bottom: 1px solid var(--border); transition: background .12s;
-}
-.module-row:last-child { border-bottom: none; }
-.module-row:hover { background: var(--surface-2); }
-.module-icon {
- width: 34px; height: 34px; border-radius: var(--r-sm);
- background: var(--surface-3); color: var(--text-3);
- display: flex; align-items: center; justify-content: center; flex-shrink: 0;
-}
-.module-icon.active { background: var(--teal-50); color: var(--teal); }
-.module-icon svg { width: 17px; height: 17px; stroke-width: 1.75; }
-.module-info { flex: 1; min-width: 0; }
-.module-name { font-size: 13.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
-.module-desc { font-size: 12px; color: var(--text-3); margin-top: 2px; }
-.module-stats { display: flex; gap: 16px; font-size: 12px; color: var(--text-3); margin-top: 3px; }
-.module-stats strong { color: var(--text-2); font-weight: 500; }
-.module-config {
- border-top: 1px solid var(--border); padding: 18px 20px;
- background: var(--surface-2);
- display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
-}
-
-/* ── Dashboard module list row ── */
-.dash-module-row {
- display: flex; align-items: center; gap: 12px;
- padding: 11px 20px; border-top: 1px solid var(--border);
-}
-.dash-module-icon {
- width: 30px; height: 30px; border-radius: var(--r-sm);
- background: var(--teal-50); color: var(--teal);
- display: flex; align-items: center; justify-content: center; flex-shrink: 0;
-}
-.dash-module-icon.off { background: var(--surface-3); color: var(--text-3); }
-.dash-module-icon svg { width: 15px; height: 15px; stroke-width: 1.75; }
-.dash-module-name { font-size: 13px; font-weight: 600; }
-.dash-module-sub { font-size: 11.5px; color: var(--text-3); }
-
-/* ── Logs ── */
-.log-controls { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
-.log-container {
- background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-lg);
- height: 500px; overflow-y: auto;
- font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12px;
-}
-.log-row {
- display: grid; grid-template-columns: 80px 48px 1fr;
- gap: 14px; padding: 6px 16px;
- border-bottom: 1px solid var(--border); align-items: start; line-height: 1.5;
-}
-.log-row:last-child { border-bottom: none; }
-.log-row:hover { background: var(--surface-3); }
-.log-time { color: var(--text-3); font-size: 11px; white-space: nowrap; padding-top: 1px; }
-.log-level { font-size: 10.5px; font-weight: 700; padding: 2px 6px; border-radius: 4px; text-align: center; text-transform: uppercase; letter-spacing: 0.04em; }
-.log-level.ok { background: rgba(52,168,83,.14); color: #2c8a44; }
-.log-level.info { background: rgba(47,125,241,.14); color: #2766c4; }
-.log-level.warn { background: rgba(232,162,58,.18); color: #b07820; }
-.log-level.err { background: rgba(229,72,77,.14); color: #c4393d; }
-[data-theme="dark"] .log-level.ok { color: #4cc471; }
-[data-theme="dark"] .log-level.info { color: #71aaff; }
-[data-theme="dark"] .log-level.warn { color: #ffc46b; }
-[data-theme="dark"] .log-level.err { color: #ff7679; }
-.log-msg { color: var(--text); word-break: break-word; }
-
-/* ── Settings ── */
-.settings-section { margin-bottom: 24px; }
-.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 14px; }
-.setting-row {
- display: flex; align-items: center; justify-content: space-between;
- padding: 14px 20px; border-bottom: 1px solid var(--border); gap: 20px;
-}
-.setting-row:last-child { border-bottom: none; }
-.setting-label { font-size: 13.5px; font-weight: 500; }
-.setting-sub { font-size: 12px; color: var(--text-3); margin-top: 2px; }
-.setting-body { padding: 18px 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
-.setting-body.full { grid-template-columns: 1fr; }
-.form-group { margin-bottom: 14px; }
-
-/* ── Password wrap ── */
-.password-wrap { position: relative; }
-.password-wrap .input { padding-right: 40px; }
-.eye-btn {
- position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
- background: none; border: none; cursor: pointer; color: var(--text-3); padding: 2px;
-}
-.eye-btn:hover { color: var(--text); }
-.eye-btn svg { width: 16px; height: 16px; stroke-width: 1.75; }
-
-/* ── Auth / Login ── */
-.auth-wrap {
- min-height: 100vh; display: flex; align-items: center; justify-content: center;
- background: var(--bg); padding: 48px 24px;
-}
-.auth-card {
- width: 100%; max-width: 380px; background: var(--surface);
- border: 1px solid var(--border); border-radius: 18px; padding: 36px 30px;
- box-shadow: var(--shadow-lg);
- display: flex; flex-direction: column; align-items: center; gap: 20px;
-}
-.auth-logo { height: 36px; }
-.auth-title { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; margin: 0; }
-.auth-sub { font-size: 13px; color: var(--text-3); text-align: center; margin-top: -10px; }
-.auth-card form { width: 100%; display: flex; flex-direction: column; gap: 12px; }
-
-/* ── Setup wizard ── */
-.wizard-wrap {
- min-height: 100vh; display: flex;
- align-items: safe center; justify-content: safe center;
- background: var(--bg); padding: 24px;
-}
-.wizard {
- width: 100%; max-width: 540px; background: var(--surface);
- border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg); overflow: hidden;
-}
-.wizard-header { padding: 26px 30px 18px; border-bottom: 1px solid var(--border); }
-.wizard-steps { display: flex; gap: 6px; margin-top: 16px; }
-.wizard-step { flex: 1; height: 3px; border-radius: 999px; background: var(--surface-3); }
-.wizard-step.done, .wizard-step.active { background: var(--teal); }
-.wizard-body { padding: 26px 30px; min-height: 220px; }
-.wizard-footer {
- padding: 14px 30px; border-top: 1px solid var(--border);
- background: var(--surface-2);
- display: flex; align-items: center; justify-content: space-between;
-}
-.wizard-icon {
- width: 48px; height: 48px; border-radius: var(--r-md);
- background: var(--teal-50); color: var(--teal);
- display: flex; align-items: center; justify-content: center;
- margin-bottom: 16px;
-}
-.wizard-icon svg { width: 24px; height: 24px; stroke-width: 1.75; }
-.wizard-title { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
-.wizard-sub { font-size: 13px; color: var(--text-3); margin-bottom: 20px; }
-.site-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
-.site-card {
- border: 2px solid var(--border); border-radius: var(--r-md); padding: 16px;
- cursor: pointer; transition: border-color .15s, background .15s;
-}
-.site-card:hover { border-color: var(--teal-50); background: var(--surface-2); }
-.site-card.selected { border-color: var(--teal); background: var(--teal-50); }
-.site-card-name { font-weight: 600; font-size: 14px; }
-.site-card-sub { font-size: 12px; color: var(--text-3); margin-top: 3px; }
-
-/* ── Pagination ── */
-.pagination { display: flex; align-items: center; gap: 3px; }
-.page-btn {
- min-width: 28px; height: 28px; border-radius: 6px;
- border: 1px solid transparent; background: transparent;
- color: var(--text-2); font-size: 12.5px; font-weight: 500; cursor: pointer;
- display: flex; align-items: center; justify-content: center; padding: 0 8px;
-}
-.page-btn:hover { background: var(--surface-3); color: var(--text); }
-.page-btn.active { background: var(--teal); color: #fff; }
-.page-btn:disabled { opacity: .4; cursor: not-allowed; }
-
-/* ── Error / Success msgs ── */
-.error-msg {
- background: rgba(229,72,77,.08); border: 1px solid rgba(229,72,77,.2);
- border-radius: var(--r-sm); padding: 10px 14px; font-size: 13px; color: var(--danger);
-}
-.success-msg {
- background: rgba(52,168,83,.08); border: 1px solid rgba(52,168,83,.2);
- border-radius: var(--r-sm); padding: 10px 14px; font-size: 13px; color: var(--success);
-}
-
-/* ── Spinner ── */
-.spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .7s linear infinite; }
-@keyframes spin { to { transform: rotate(360deg); } }
-
-/* ── HTMX ── */
-.htmx-indicator { display: none; }
-.htmx-request .htmx-indicator { display: inline-flex; }
-
-/* ── Utilities ── */
-.spacer { flex: 1; }
-.mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }
diff --git a/nexus-scanner/internal/web/templates/ad.html b/nexus-scanner/internal/web/templates/ad.html
deleted file mode 100644
index 9aab226..0000000
--- a/nexus-scanner/internal/web/templates/ad.html
+++ /dev/null
@@ -1,71 +0,0 @@
-{{define "content"}}
-
-
-
Active Directory
-
{{len .Computers}} Computer-Objekte aus {{.Domain}}
-
-
-
-{{if not .ADEnabled}}
-
-
AD Sync nicht konfiguriert
-
Trage Server, Bind-DN und Passwort in der config.yaml ein und aktiviere das Modul.
-
-{{end}}
-
-
-
-
-
Computer in AD
-
{{len .Computers}}
-
{{.Domain}}
-
-
-
Im Netzwerk gesehen
-
{{.SeenCount}}
-
↑ aktiv erreichbar
-
-
-
Nicht gesehen
-
{{.MissingCount}}
-
offline oder abwesend
-
-
-
-
-
-
-
-
Computer-Name
-
Abteilung / OU
-
Betriebssystem
-
Letzter Login (AD)
-
Im Netzwerk
-
-
-
- {{range .Computers}}
-
-
{{.CN}}
-
{{if .Department}}{{.Department}}{{else}}—{{end}}
-
{{if .OS}}{{.OS}}{{else}}—{{end}}
-
- {{if not .LastLogon.IsZero}}{{formatTime .LastLogon}}{{else}}—{{end}}
-