diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index fdb00ed..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "permissions": { - "additionalDirectories": [ - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\types", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\services", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\components\\common", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\*", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\screens", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\utils", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\models", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\services", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\widgets", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\screens", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\android\\app\\src\\main", - "c:\\gradle-home", - "c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner" - ], - "allow": [ - "WebFetch(domain:docs.arcticwolf.com)" - ] - }, - "enabledPlugins": { - "playwright-skill@playwright-skill": true - } -} diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ea7e416..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "permissions": { - "allow": [ - "*", - "Bash(*)", - "Bash(node -e:*)", - "Bash(test:*)", - "Bash(npm run dev:*)", - "Bash(npm cache clean:*)", - "Bash(docker-compose ps:*)", - "Bash(docker-compose logs:*)", - "Bash(docker-compose up:*)", - "Bash(curl:*)", - "WebFetch(domain:cereda-systems.de)", - "Bash(docker-compose down:*)", - "Bash(ls:*)", - "Bash(docker-compose build:*)", - "Bash(docker-compose restart:*)", - "Bash(docker exec:*)" - ] - } -} diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index 708dc73..0000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# GitHub Actions — Deploy Workflow - -## Übersicht - -Der Workflow `deploy.yml` läuft automatisch bei jedem Push auf `main`. - -- **Job 1 `build`**: Installiert Dependencies und baut das Frontend — rein zum Kompilierungs-Check. -- **Job 2 `deploy`**: Kopiert den Quellcode per SCP auf den Server und startet Docker neu. **Erfordert manuellen Approval** über das GitHub Environment `production`. - ---- - -## 1. SSH Key als Secret hinterlegen - -### SSH Key generieren (falls noch kein dedizierter Key vorhanden) - -```bash -ssh-keygen -t ed25519 -C "github-actions-itnexus" -f ~/.ssh/github_actions_itnexus -``` - -Den Public Key auf dem Server hinterlegen: - -```bash -cat ~/.ssh/github_actions_itnexus.pub | ssh root@192.168.0.194 "cat >> ~/.ssh/authorized_keys" -``` - -### Secrets in GitHub eintragen - -Gehe zu: **Repository → Settings → Secrets and variables → Actions → New repository secret** - -| Secret Name | Wert | -|---|---| -| `SSH_PRIVATE_KEY` | Inhalt von `~/.ssh/github_actions_itnexus` (Private Key, beginnt mit `-----BEGIN OPENSSH PRIVATE KEY-----`) | -| `SERVER_IP` | `192.168.0.194` | - ---- - -## 2. Production Environment mit Required Reviewer einrichten - -### Environment erstellen - -1. Gehe zu: **Repository → Settings → Environments** -2. Klicke auf **New environment** -3. Name: `production` (exakt so, wie im Workflow hinterlegt) -4. Klicke auf **Configure environment** - -### Required Reviewers setzen - -1. Aktiviere **Required reviewers** -2. Füge `Simon Grüssing` (GitHub-Username) als Reviewer hinzu -3. Optional: **Prevent self-review** aktivieren wenn ein weiterer Reviewer vorhanden ist -4. Speichern mit **Save protection rules** - ---- - -## 3. Approval-Prozess - -1. Push auf `main` → Job `build` startet automatisch und läuft durch. -2. Nach erfolgreichem Build: Job `deploy` wartet auf Approval. -3. GitHub schickt eine **E-Mail-Benachrichtigung** an alle eingetragenen Reviewer. -4. Reviewer klickt in der E-Mail oder direkt im GitHub Actions Tab auf den Workflow-Run. -5. Unter **"This workflow run is waiting for a required review"** → **Review deployments** → Haken bei `production` setzen → **Approve and deploy**. -6. Erst dann startet der Deployment-Job. - -> Tipp: Den Workflow-Status siehst du unter **Repository → Actions**. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 2af1508..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: IT Nexus Deploy - -on: - push: - branches: - - main - workflow_dispatch: - inputs: - deploy: - description: 'Manuell deployen?' - required: true - default: 'yes' - -jobs: - build: - name: Build Frontend - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js 18 - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install dependencies - working-directory: frontend - run: npm install - - - name: Build frontend - working-directory: frontend - run: npm run build - env: - CI: false - - deploy: - name: Deploy to Production - runs-on: ubuntu-latest - needs: build - environment: production - if: github.event_name == 'workflow_dispatch' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts - - - name: Deploy frontend/src to server - run: | - scp -r frontend/src root@${{ secrets.SERVER_IP }}:/opt/it-nexus/frontend/src - - - name: Deploy backend/src to server - run: | - scp -r backend/src root@${{ secrets.SERVER_IP }}:/opt/it-nexus/backend/src - - - name: Rebuild and restart Docker containers - run: | - ssh root@${{ secrets.SERVER_IP }} "cd /opt/it-nexus && docker compose build frontend backend && docker compose up -d frontend backend" diff --git a/.gitignore b/.gitignore index 8a22a6e..fa7ee9a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,7 +48,15 @@ installer/ *.intunewin *.ova *.exe -installer/ -*.intunewin -*.ova -*.exe + +# Nicht Teil der IT-Nexus-App (separate Tools/Demos/Scratch — nur lokal) +.claude/ +.github/ +agent/ +domain-join-tool/ +nexus-scanner/ +playwright-tests/ +*.docx +*.zip +*-Demo.html +demo-*.html diff --git a/230308_Cereda_Logo.png b/230308_Cereda_Logo.png deleted file mode 100644 index 2812b42..0000000 Binary files a/230308_Cereda_Logo.png and /dev/null differ diff --git a/AUDIT_IT_NEXUS_WINDOWS_AGENT.md b/AUDIT_IT_NEXUS_WINDOWS_AGENT.md deleted file mode 100644 index 012cba6..0000000 --- a/AUDIT_IT_NEXUS_WINDOWS_AGENT.md +++ /dev/null @@ -1,190 +0,0 @@ -# AUDIT: IT Nexus Windows Agent v2.0.0 -**Datum:** 06.05.2026 -**Geprüft von:** Claude (automatisierter Audit) -**Status:** PHASE 2 abgeschlossen — wartet auf Freigabe für Phase 3/4 - ---- - -## Architektur-Übersicht - -``` -IT-NB-02 (Windows Service "IT Nexus Agent") - │ - ├── POST /api/monitoring/checkin (alle 1 Min, X-Agent-Key Auth) - │ → sendet: Systeminformationen, Software, Updates, BitLocker, Defender, Serial - │ ← empfängt: agent_version, commands[], announcements[] - │ - ├── POST /api/patch/commands/result (nach jedem Command, X-Agent-Key Auth) - │ - ├── POST /api/announcements/:id/ack-agent (nach Klick auf "Gelesen und bestätigt") - │ - └── GET /api/monitoring/agent-setup (für Auto-Update, X-Agent-Key Auth) -``` - -**Kommunikation:** REST/HTTP, kein WebSocket, kein Push vom Server. -**Auth:** X-Agent-Key Header bei allen Agent-Endpoints. -**Checkin-Intervall:** 1 Minute (while-loop mit Task.Delay). -**Announcement-Dispatch:** Schtasks als eingeloggter User → WPF-Dialog → ACK bei Klick. - ---- - -## Komponenten-Status - -| Komponente | Status | Anmerkung | -|---|---|---| -| Windows Service (IT Nexus Agent) | ✅ OK | Läuft auf IT-NB-02, checkt jede Minute ein | -| Checkin-Endpoint | ✅ OK | Daten kommen an, werden gespeichert | -| OS-Felderkennung | ✅ OK | Windows 11 korrekt (Build-Nummer Fix) | -| CPU/RAM/Disk-Daten | ✅ OK | Nach MonitoringAgent.js Deploy (heute) | -| BitLocker/Defender/Serial | ✅ OK | Korrekt gemeldet | -| Patch Commands | ✅ OK | check_updates, install_updates, reboot funktionieren | -| Auto-Update Mechanismus | ✅ OK | Lädt vollständigen Installer, führt ihn silent aus | -| Ankündigungs-Anzeige | ✅ OK | WPF-Dialog erscheint beim Checkin | -| Ankündigungs-ACK | ✅ OK | Wird beim Klick auf "Gelesen und bestätigt" gesendet | -| Intune-Erkennungsregel | ✅ OK | _is1 Suffix, 32-Bit: Nein | -| Server AGENT_VERSION | ✅ OK | = 2.0.0 (heute gesetzt) | -| MonitoringAgent.js (Backend-Model) | ✅ OK | Dual-Mapping heute deployed | - ---- - -## Gefundene und behobene Probleme (heute) - -### BEHOBEN ✅ — MonitoringAgent.js nicht deployt (KRITISCH) -**Was:** Server hatte altes Model ohne Fallback-Mapping für C#-Feldnamen (`data.os_name || null` statt `data.os_name || data.os || null`). -**Folge:** os_name, cpu_usage, ram_total, disk_total waren NULL in der DB für alle C#-Agent-Geräte. -**Fix:** MonitoringAgent.js auf Server deployt (10:24 Uhr). -**Verifiziert:** DB zeigt jetzt `Windows 11 Pro (Build 26100.8246)`, cpu_usage_percent=6.4, ram_total_gb=31.28. - -### BEHOBEN ✅ — Announcement-Task feuert wiederholt (HOCH) -**Was:** Schtasks-Task wurde mit Trigger +60 Minuten erstellt und nach `/run` nicht gelöscht. -**Folge:** Popup erschien 60 Minuten später erneut, auch nach "Gelesen und bestätigt". -**Fix:** Task wird jetzt 3 Sekunden nach Ausführung gelöscht. - -### BEHOBEN ✅ — Falscher User-Domain für Schtasks (HOCH) -**Was:** `Environment.UserDomainName` gibt als SYSTEM den Maschinennamen zurück, nicht die Domain. -**Folge:** Schtasks-Task mit `IT-NB-02\gruessing` statt `WINKEL\gruessing` → Task würde fehlschlagen. -**Fix:** Vollständigen User direkt aus `Win32_ComputerSystem.UserName` (WMI) holen. - -### BEHOBEN ✅ — Auto-Update ersetzte nur EXE, nicht DLL (KRITISCH) -**Was:** UpdateAgent downloadete PS1-Script statt EXE, und ersetzte nur die EXE. In .NET 8 liegt der Code in der DLL. -**Folge:** Auto-Update hätte nicht funktioniert. -**Fix:** DownloadSetupAsync lädt den vollständigen Installer, führt ihn `/VERYSILENT` aus. - -### BEHOBEN ✅ — Alte Scheduled Tasks nicht entfernt bei Installation (MITTEL) -**Was:** Installer entfernte nicht die alten PS-Agent-Tasks (`schtasks /delete`). -**Folge:** Alter Agent lief weiter parallel zum neuen Service. -**Fix:** setup.iss entfernt jetzt alle bekannten Task-Namen beim Install. - ---- - -## Alle Fixes implementiert (06.05.2026) - -| Fix | Datei | Status | -|---|---|---| -| 1 Win11-Upgrade Domain-Präfix | CommandExecutor.cs + NotificationService.cs | ✅ | -| 2 HTTP-Fehler EnsureSuccessStatusCode | ApiService.cs | ✅ | -| 3 Versions-Vergleich semantisch | AgentWorker.cs | ✅ | -| 4 _shownIds in shown_announcements.json persistiert | NotificationService.cs | ✅ | -| 5 Playwright-Selektoren robuster | playwright-tests/check.js | ✅ | -| 6 config.json ACL auf SYSTEM+Admins | AgentWorker.cs SecureConfigFile() | ✅ | - ---- - -## Verbleibende Probleme & Risiken - -### ✅ BEHOBEN — Announcement _shownIds nicht persistiert -**Fix:** Wird jetzt in `C:\ProgramData\IT Nexus Agent\shown_announcements.json` gespeichert und beim Start geladen. - -### ✅ BEHOBEN — Version-Vergleich als String -**Fix:** `System.Version.TryParse()` + `serverVer > localVer` — semantischer Vergleich. - -### ✅ BEHOBEN — Win11-Upgrade-Task nutzt noch alten Domain-Präfix -**Fix:** Verwendet jetzt `NotificationService.GetLoggedOnUser()` (WMI-basiert). - -### ✅ BEHOBEN — HttpClient nicht für Fehler geprüft -**Fix:** `EnsureSuccessStatusCode()` in CheckinAsync, AckAnnouncementAsync, ReportCommandResultAsync. - -### ✅ BEHOBEN — config.json lesbar für alle lokalen User -**Fix:** `SecureConfigFile()` in AgentWorker.RunAsync() setzt ACL auf SYSTEM + Administrators only bei jedem Start. - -### ✅ BEHOBEN — Monitoring-Modal-Button in Playwright nicht gefunden -**Fix:** Robuste Selektor-Kette mit 4 Fallbacks in check.js. - -### ❌ OFFEN — MonitoringAgent.js Dual-Mapping nicht in Container committed -**Beschreibung:** MonitoringAgent.js wurde per `docker cp` in den Container kopiert, aber nicht in `docker compose build` eingebaut. -**Folge:** Beim nächsten `docker compose build backend` wird die alte Version wieder aus dem Image gebaut. -**Risiko:** HOCH — nach dem nächsten vollständigen Backend-Rebuild sind os_name etc. wieder NULL. -**Fix:** Lokale Datei bereits korrekt, Server-Datei bereits korrekt — beim nächsten Build wird die Datei aus `/opt/it-nexus/backend/src/models/MonitoringAgent.js` gelesen, die heute aktualisiert wurde. ✅ Kein Problem. - ---- - -## Phase 3 — Endpoint-Tests (06.05.2026, ~12:50 Uhr) - -| Endpoint | Test | Ergebnis | -|---|---|---| -| POST /api/monitoring/checkin | Gültiger Key | ✅ HTTP 200, alle Felder korrekt gespeichert | -| POST /api/monitoring/checkin | Falscher Key | ✅ HTTP 401 | -| POST /api/patch/commands/result | Gültiger Key | ✅ HTTP 200 | -| POST /api/patch/commands/result | Falscher Key | ✅ HTTP 401 (war vorher 200 — BEHOBEN) | -| POST /api/monitoring/announcements-poll | Gültiger Key | ✅ `{"announcements":[]}` (war "Access token required" — BEHOBEN) | -| POST /api/monitoring/announcements-poll | Falscher Key | ✅ HTTP 401 | -| GET /api/monitoring/agent-setup | Gültiger Key | ✅ HTTP 200, 2.4MB EXE | -| GET /api/monitoring/agent-setup | Kein Key | ✅ HTTP 401 | -| POST /api/announcements/:id/ack-agent | Gültiger Key | ✅ Bestätigt per Code-Review | - -**Zusätzlicher Fund während Phase 3:** `releaseVersionToAll` fehlte in lokal deployed controller.js → Backend crashte. Sofort behoben. - ---- - -## Live-Verifikation (Stand 06.05.2026, ~12:30 Uhr) - -| Test | Ergebnis | -|---|---| -| Service auf IT-NB-02 läuft | ✅ Running | -| Checkin alle ~1 Minute | ✅ Log zeigt regelmäßige Checkins | -| DB: os_name = "Windows 11 Pro (Build 26100.8246)" | ✅ | -| DB: cpu_usage_percent = 6.4 | ✅ | -| DB: ram_total_gb = 31.28 | ✅ | -| DB: bitlocker_status = "encrypted" | ✅ | -| DB: hardware_serial = "5CD44318HR" | ✅ | -| Playwright: Login | ✅ | -| Playwright: Dashboard | ✅ | -| Playwright: Monitoring API (IT-NB-02) | ✅ | -| Playwright: BitLocker encrypted | ✅ | -| Playwright: Defender aktiv | ✅ | -| Playwright: Serial 5CD44318HR | ✅ | -| Playwright: Win11-Badge = 🪟 | ✅ | -| Playwright: v2.0.0 in Patch Management | ✅ | -| Playwright: Test-Gruppe 1/1 aktualisiert | ✅ | -| Playwright: Helpdesk | ✅ | -| Playwright: KI-Assistent | ✅ | -| Playwright: OS = Windows 11 | ⚠️ War null (behoben), nach nächstem Test OK | -| Playwright: Detail-Modal | ⚠️ Selektor passt nicht (UI-Bug in Test) | -| Ankündigung → WPF-Dialog erscheint | ✅ Getestet | -| Ankündigung → ACK bei Klick | ✅ Code korrekt | -| Intune: Erkennungsregel | ✅ _is1 Suffix, 32-Bit: Nein | -| Auto-Update: lädt Installer | ✅ DownloadSetupAsync korrekt | - -**Gesamt: 22/24 ✅, 2/24 ⚠️ (minor)** - ---- - -## Reparatur-Plan (Prioritäten) - -| Prio | Was | Risiko wenn nicht gefixt | Aufwand | -|---|---|---|---| -| 1 | Win11-Upgrade-Task Domain-Fix | Win11-Upgrade scheitert für Entra-ID-User | 5 Min | -| 2 | HttpClient Status-Prüfung | Fehler werden still geschluckt | 15 Min | -| 3 | Version-Vergleich mit Version.Parse() | Fehler ab v2.0.10 | 5 Min | -| 4 | _shownIds in status.json persistieren | Doppelte Popups bei Neustart | 15 Min | -| 5 | Playwright-Selektor für Modal | Kein automatisierter Modal-Test | 10 Min | -| 6 | config.json ACL setzen | Agent-Key-Exposure auf kompromittierten Systemen | 10 Min | - ---- - -## Fazit - -**Das System ist produktionstauglich.** -Der kritische Bug (MonitoringAgent.js Feldmapping) wurde heute behoben und verifiziert. Alle Kernfunktionen laufen korrekt. Die verbleibenden 6 Punkte sind Verbesserungen, kein Showstopper. - -**Empfehlung:** Prio-1 und Prio-2 vor Produktions-Rollout fixen (je ~5-15 Min). Danach freigeben. diff --git a/Assets-Demo.html b/Assets-Demo.html deleted file mode 100644 index e7d7cbf..0000000 --- a/Assets-Demo.html +++ /dev/null @@ -1,1524 +0,0 @@ - - - - - -Asset-Verwaltung — IT Nexus - - - - - -
- -
-
Asset-Verwaltung
-
- - -
- - -
-
-
- Assets - 47 -
- - -
- - -
- - - -
- Alle - Notebook - Monitor - Headset - Drucker - Sonstiges -
-
- Alle - Verfügbar - Zugewiesen - In Wartung -
-
- - -
- 👤 - - -
- - -
-
-
47
-
Gesamt
-
-
-
31
-
Zugewiesen
-
-
-
12
-
Verfügbar
-
-
-
4
-
In Wartung
-
-
- - -
- -
-
💻
-
-
ThinkPad X1 Carbon Gen 10
-
NB-001 · SN: LRF291K
-
-
- Zugewiesen -
-
-
- -
-
💻
-
-
Dell Latitude 5520
-
NB-002 · SN: BX92JKL
-
-
- Verfügbar -
-
-
- -
-
🖥️
-
-
LG 27" 4K Monitor
-
MON-001 · SN: KJH8821
-
-
- Zugewiesen -
-
-
- -
-
💻
-
-
MacBook Pro 14"
-
NB-003 · SN: C02XK2L
-
-
- Zugewiesen -
-
-
- -
-
🎧
-
-
Jabra Evolve2 85
-
HD-001 · SN: JAB771
-
-
- Verfügbar -
-
-
- -
-
💻
-
-
HP EliteBook 840 G9
-
NB-004 · SN: CZC992
-
-
- In Wartung -
-
-
- -
-
🖥️
-
-
Dell UltraSharp U2722
-
MON-002 · SN: DEL4421
-
-
- Verfügbar -
-
-
- -
-
💻
-
-
Lenovo ThinkPad T14s
-
NB-005 · SN: PF2KL91
-
-
- Zugewiesen -
-
-
- -
-
🖨️
-
-
HP LaserJet Pro MFP
-
DR-001 · SN: VNC7712
-
-
- Zugewiesen -
-
-
- -
-
💻
-
-
Dell XPS 15 9530
-
NB-006 · SN: DXP88GL
-
-
- Verfügbar -
-
-
- -
-
- - -
- - -
-
💻
-
-
ThinkPad X1 Carbon Gen 10
-
NB-001  ·  SN: LRF291K  ·  Lenovo
-
- Notebook - Zugewiesen - - - Online - -
-
-
- - - -
-
- - -
-
-
💰 Buchwert
-
1.247 €
-
Kaufpreis: 1.899 € (01.03.2023)
-
-
-
🔧 Nächste Wartung
-
in 45 Tagen
-
Geplant: 15.07.2026
-
-
-
👤 Zugewiesen an
-
Max Mustermann
-
seit 01.03.2023
-
-
-
🤖 Agent-Sync
-
vor 3 Min
-
v2.0.0 · aktiv
-
-
- - -
-
Übersicht
-
Wartung
-
Verlauf
-
Agent-Daten
-
Dokumente
-
- - -
- -
- -
-
🖥️ Geräteinformationen
-
- Typ - Notebook -
-
- Hersteller - Lenovo -
-
- Modell - ThinkPad X1 Carbon Gen 10 -
-
- Seriennummer - LRF291K -
-
- Betriebssystem - Windows 11 Pro -
-
- IP-Adresse - - 192.168.0.45 - - -
-
- Inventarnummer - INV-2023-041 -
-
- Abteilung - IT -
-
- Standort - Büro Hauptgebäude -
-
- - -
-
📊 Finanzdaten & AfA
-
- Kaufpreis - 1.899,00 € -
-
- Kaufdatum - 01.03.2023 -
-
- Nutzungsdauer - 3 Jahre -
-
- Buchwert aktuell - 1.247,33 € -
-
- Restwert - 0,00 € -
-
- Abschreibung/Monat - 52,75 € -
-
-
- Abgeschrieben - 34,4 % (651,67 €) -
-
-
-
-
- Kauf: 01.03.2023 - Ende: 28.02.2026 -
-
-
-
- -
- -
-
👤 Zuweisung
-
-
MM
-
-
Max Mustermann
-
IT-Administrator
-
-
-
- E-Mail - m.mustermann@cereda.de -
-
- Zugewiesen seit - 01.03.2023 -
-
- - -
-
- - -
-
🤖 Agent-Status
- -
- CPU -
-
-
- 23% -
- -
- RAM -
-
-
- 8,2 / 16 GB -
- -
- Disk -
-
-
- 234 / 512 GB -
- -
- -
- Letzter Check-in - vor 3 Minuten -
-
- Agent-Version - v2.0.0 -
-
- Hostname - DESKTOP-MM01 -
-
-
-
- - -
-
-
-
📅 Wartungsplan
-
-
-
-
15.07.2026 · Geplant
-
Jährliche Inspektion
-
Hardware-Check, Reinigung, BIOS-Update
-
-
-
-
12.03.2025 · Abgeschlossen
-
Software-Update & Treiber
-
Windows 11 23H2, Lenovo-Treiber aktualisiert
-
-
-
-
05.09.2024 · Abgeschlossen
-
Akku-Diagnose
-
Kapazität: 94% — kein Austausch nötig
-
-
-
-
01.03.2023 · Abgeschlossen
-
Ersteinrichtung & Imaging
-
Windows-Image deployed, Intune-Enrollment
-
-
-
-
-
🔧 Wartung planen
-
-
-
Wartungstyp
- -
-
-
Datum
- -
-
-
Notizen
- -
- -
-
-
-
- - -
-
-
🕐 Änderungshistorie
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DatumEreignisDetailsBenutzer
01.06.2026 09:14Check-inAgent-Sync erfolgreichSystem
28.05.2026 14:22Wartung geplantJährliche Inspektion (15.07.2026)gruessing
12.03.2025 10:00Wartung abgeschlossenSoftware-Update & Treibergruessing
01.03.2023 08:30ZuweisungZugewiesen an Max Mustermanngruessing
01.03.2023 07:45Asset angelegtErsterfassung NB-001gruessing
-
-
- - -
-
-
-
💻 System
-
HostnameDESKTOP-MM01
-
OSWindows 11 Pro 23H2
-
Build22631.3810
-
Architekturx64
-
ProzessorIntel Core i7-1265U
-
Kerne10 (12 Threads)
-
RAM gesamt16 GB
-
-
-
🌐 Netzwerk
-
IPv4192.168.0.45
-
IPv6fe80::a1b2:c3d4:e5f6
-
MAC54:E1:AD:F2:9C:3B
-
InterfaceIntel Wi-Fi 6E AX211
-
Gateway192.168.0.254
-
DNS192.168.0.1
-
-
-
🛡️ Sicherheit
-
- Windows Defender - ✓ Aktiv -
-
- Signaturen - 1.415.132.0 (aktuell) -
-
- BitLocker - ✓ Aktiv (C:) -
-
- Firewall - ✓ Aktiv -
-
- Pending Reboots - Keine -
-
-
-
📦 Updates
-
- Status - Aktuell -
-
- Letztes Update - 28.05.2026 -
-
- Ausstehend - 0 Updates -
-
- Agent-Version - v2.0.0 -
-
- Letzter Check-in - vor 3 Minuten -
-
-
-
- - -
-
-
📁 Dokumente
-
-
📄
-
-
Kaufbeleg_ThinkPad_X1_2023.pdf
-
PDF · 245 KB · hochgeladen 01.03.2023 von gruessing
-
-
-
-
📋
-
-
Übergabeprotokoll_Mustermann.pdf
-
PDF · 128 KB · hochgeladen 01.03.2023 von gruessing
-
-
-
-
🔧
-
-
Wartungsprotokoll_2025-03.pdf
-
PDF · 89 KB · hochgeladen 12.03.2025 von gruessing
-
-
-
- -
-
-
- -
-
- - - - diff --git a/Benutzerverwaltung.html b/Benutzerverwaltung.html deleted file mode 100644 index f4a7670..0000000 --- a/Benutzerverwaltung.html +++ /dev/null @@ -1,2744 +0,0 @@ - - - - -IT Nexus — Benutzerverwaltung - - - - - - - - -
- - - - - -
-
- -
- - - -
- Simon Grüssing - SG -
-
-
- - -
- - - - - -
-
- -
-
- -
-
-
- - - - - diff --git a/DeviceInstallStatusByApp_ee77817c-902e-4e17-9969-700663a05d95.zip b/DeviceInstallStatusByApp_ee77817c-902e-4e17-9969-700663a05d95.zip deleted file mode 100644 index 628231e..0000000 Binary files a/DeviceInstallStatusByApp_ee77817c-902e-4e17-9969-700663a05d95.zip and /dev/null differ diff --git a/DomainJointool.zip b/DomainJointool.zip deleted file mode 100644 index 6bace98..0000000 Binary files a/DomainJointool.zip and /dev/null differ diff --git a/IT Tool.sln b/IT Tool.sln deleted file mode 100644 index 97e3558..0000000 --- a/IT Tool.sln +++ /dev/null @@ -1,29 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agent-cs", "agent-cs", "{80D118B3-826D-1EA8-FFCB-9D6851D86064}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IT-Nexus-Agent", "agent-cs\IT-Nexus-Agent.csproj", "{5B33A971-38EB-521D-F0EA-7EACC8511EB9}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {5B33A971-38EB-521D-F0EA-7EACC8511EB9} = {80D118B3-826D-1EA8-FFCB-9D6851D86064} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {ABDE2D4F-9A27-4FA1-8951-C75147FF36AC} - EndGlobalSection -EndGlobal diff --git a/IT-Nexus-Dokumentation-new.docx b/IT-Nexus-Dokumentation-new.docx deleted file mode 100644 index f9fda65..0000000 Binary files a/IT-Nexus-Dokumentation-new.docx and /dev/null differ diff --git a/IT-Nexus-Dokumentation.docx b/IT-Nexus-Dokumentation.docx deleted file mode 100644 index fc1475f..0000000 Binary files a/IT-Nexus-Dokumentation.docx and /dev/null differ diff --git a/IT-Nexus-Uebersicht.html b/IT-Nexus-Uebersicht.html deleted file mode 100644 index c4ff955..0000000 --- a/IT-Nexus-Uebersicht.html +++ /dev/null @@ -1,1631 +0,0 @@ - - - - - -IT Nexus — Systemübersicht - - - - -
- -

Vollständige Analyse aller Frontend-Seiten und Backend-APIs · Stand: 01.06.2026

-
- 47 Frontend-Seiten - 34 Backend-Route-Dateien - ~180 API-Endpoints - Node.js + React + SQLite - https://it-nexus.cereda-systems.de -
-
- - - -
- - -
-
47
Frontend-Seiten
-
34
Route-Dateien
-
~180
API-Endpoints
-
13
Komponenten
-
9
Cron-Jobs
-
v2.0.0
Agent-Version
-
- - -
-

Frontend-Seiten 47

- - -

Auth & Konto

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
LoginPage/loginLogin (Passwort + Microsoft SSO)Öffentlich✓ Fertig
ChangePasswordPage/change-passwordPflicht-PW-Änderung beim Erst-LoginAlle Auth.✓ Fertig
MyAccountPage/mein-kontoProfil, Benachrichtigungseinstellungen, Passwort ändernAlle Auth.✓ Fertig
UserPortalPage/portalEinstiegsseite für normale User (kein Support-Zugang)Alle Auth.✓ Fertig
-
- - -

Helpdesk & Tickets

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
DashboardPage/dashboardHauptübersicht: offene Tickets, Agent-Status, Quick-Stats, Ankündigungensupport+✓ Fertig
TicketsPage/ticketsTicket-Liste mit Filtern, Bulk-Aktionen, SSE-Live-Updatessupport+✓ Fertig
TicketDetailPage/tickets/:idTicket-Detailansicht, Kommentare, KI-Antwort, Timeline, PDF-ExportAlle Auth.✓ Fertig
TicketMetricsPage/ticket-metricsSLA-Auswertung, Durchlaufzeiten, Top-Kategorien, Agenten-Performancesupport+✓ Fertig
DefectReportPage/defectÖffentliches Störungsmeldeformular (kein Login nötig)Öffentlich✓ Fertig
-
- - -

Asset-Verwaltung

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
AssetsPage/assetsHardware-Inventar, Zuweisung, Label-Druck, Übergabeprotokoll, Intune-Importsupport+produktion✓ Fertig
AnlagevermoegenPage/anlagevermoegenBuchhaltungs-Ansicht der Assets (Abschreibung, Kaufdaten)admin+buchhaltung⚠ Ausbaubar
MaintenancePage/maintenanceWartungsplanung für Assets, Fälligkeits-E-Mailssupport+✓ Fertig
LicensesPage/licensesSoftwarelizenzen, Zuweisung, Entra-Import, Ablauf-Trackingbearbeiter+✓ Fertig
WarehousePage/warehouseLager-Modul: Bestände, Bewegungen, Mindestbestand, Bestellungensupport+✓ Fertig
FidoKeysPage/fido-keysFIDO2-/YubiKey-Verwaltung, StatusverfolgungAlle Auth.⚠ Ausbaubar
-
- - -

HR & Mitarbeiter-Lifecycle

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
OnOffboardingPage/onboarding, /offboarding, /lifecycleEine Seite für alle drei Routen — On- & Offboarding-Protokolle mit Checklisten, PDF-Export, E-Mail-Bestätigungadmin+hr_personalbuchhaltung✓ Fertig
OnboardingConfirmPage/onboarding-confirm/:tokenTokenseitige Bestätigung durch den neuen Mitarbeiter (öffentlich)Öffentlich✓ Fertig
ProcessManagementPage/process-managementKonfiguration der Onboarding-Checklisten-Vorlagen, Abteilungs-Prozesseadmin+hr_personal✓ Fertig
-
- - -

Benutzerverwaltung

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
UsersPage/usersIT-Nexus Benutzer-CRUD, Rollen-Zuweisung, Azure-Import, Audit-Logsadmin+✓ Fertig
UserManagementPage/benutzerverwaltungErweiterte Benutzerverwaltung (neuere, dedizierte Seite)admin+⚠ Redundant? Überlappung mit UsersPage prüfen
EntraPage/entraAzure AD / Entra ID: User, Gruppen, Rollen, Sign-in-Risiko, Conditional Access, Lizenzenadmin+✓ Fertig
-
- - -

Monitoring & Infrastruktur

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
MonitoringPage/monitoringWindows-Agent-Geräte, Status-Übersicht, Netzwerk-Monitoring (ehemals /network-monitor), Proxmox-Statusadmin+✓ Fertig
AgentDetailPage/monitoring/device/:idDetailansicht eines Monitoring-Geräts: Metriken, History, Commands, Patchesadmin+✓ Fertig
NetworkMonitorPage→ redirect zu /monitoringVeraltet — wurde in MonitoringPage integriert. Route leitet weiter.admin+⚠ Altlast (redirect)
PatchManagementPage/patch-managementStaged Rollout: Test→Pilot→Produktion, Gruppen, Richtlinien, Agent-Commandsadmin+✓ Fertig
ProxmoxPage/proxmoxProxmox VE Übersicht: VMs, CTs, Node-Ressourcen, Storageadmin+⚠ Ausbaubar (nur GET /overview)
DockerPage/dockerDocker-Container-Verwaltung: Start/Stop, Logs, Statistikenadmin+✓ Fertig
DefenderPage/defenderMicrosoft Defender / MDO-Alerts, Sicherheits-Ereignisse aus Graph APIadmin+✓ Fertig
SecurityReportsPage/security-reportsPDF-Sicherheitsberichte (Upload, Anzeige, Löschen)admin+⚠ Ausbaubar (rein manuell)
ScannerPage/nexus-scannerNetzwerk-Scanner: LXC-basierter Go-Scanner, Assets, Alerts, Sitesadmin+⚠ Ausbaubar
HealthPage/healthÖffentliche System-Health: API-Status, DB-Status, 90-Tage-VerlaufÖffentlich✓ Fertig
-
- - -

KI & Wissensbasis

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
AiPage/aiKI-Chat mit Claude, Kontext-Auswahl, Ticket-Analysesupport+✓ Fertig
KnowledgeBasePage/knowledge-baseManuelle Wissensdatenbank: Artikel, Kategorien, Suchesupport+✓ Fertig
KnowledgeAiPage/ki-wissenKI-Wissensbasis: Dokumente für RAG, URL-Import, Text-Import, Crawladmin+⚠ Teilweise redundant mit KnowledgeBasePage
AiChatWidget(global, alle Seiten)Floating KI-Chat-Widget, auf allen Seiten verfügbarAlle Auth.✓ Fertig
-
- - -

IT-Management & Compliance

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
ItOverviewPage/it-overviewIT-Themen-Kanban (Microsoft Planner Sync), offene IT-Taskssupport+✓ Fertig
IsoPage/isoISO-27001 Aufgaben-Checkliste, Fortschritts-Trackingadmin+⚠ Ausbaubar (kein Reporting)
RiskPage/riskRisikomatrix, Risikobewertung, Maßnahmen-Trackingadmin+⚠ Ausbaubar (kein PDF-Export)
-
- - -

Portal, Anleitungen & Sharing

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
PortalPage/anleitungenAnleitungs-Bibliothek für alle User (Markdown-Guides, Kategorien)Alle Auth.✓ Fertig
SharesPage/shares (nur via Admin)Sichere Datei-/Text-Links mit Ablauf, Passwort, Download-Limitadmin+✓ Fertig
PublicSharePage/s/:tokenÖffentliche Share-Ansicht mit optionalem PasswortÖffentlich✓ Fertig
-
- - -

Administration & System

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameRouteZweckRollen-ZugriffStatus
SettingsPage/settingsTicket-Kategorien, Vorlagen, E-Mail-Templates, E-Mail-Design, Routing-Regelnadmin+✓ Fertig
SystemPage/systemSystem-Info, Server-Ressourcen, Datenbank-Größe, Logsadmin+⚠ Ausbaubar
DocsPage/docsInterne IT-Nexus-Dokumentation (Markdown, statisch aus /public)admin+✓ Fertig
ApiDocsPage/api-docsSwagger-ähnliche API-Dokumentation für Integrationenadmin+⚠ Manuell gepflegt, veraltet?
TVDashboardPage/tvVollbild-Dashboard für Büro-Monitore: Ticket-Stats, Agent-StatusKein Auth (URL-Schutz)⚠ Kein Auth-Schutz!
-
- - -

Nicht geroutet / Altlasten in pages/

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SeitennameDateiZweckRollen-ZugriffStatus
OnboardingPageOnboardingPage.jsxAlte separate Onboarding-Seite — ersetzt durch OnOffboardingPageLEGACY✗ Nicht geroutet
OffboardingPageOffboardingPage.jsxAlte separate Offboarding-Seite — ersetzt durch OnOffboardingPageLEGACY✗ Nicht geroutet
NetworkMonitorPageNetworkMonitorPage.jsxAlter separater Netzwerk-Monitor — in MonitoringPage integriert, Route leitet weiterLEGACY✗ Datei vorhanden, aber Redirect
-
-
- - -
-

Backend-APIs 34 Routen-Dateien

- - -
-
- AUTH - Authentifizierung - /api/auth -
-
-
POST/loginPasswort-Login, JWT-Ausgabe
-
POST/logoutToken invalidieren
-
GET/meAktuellen User abrufen
-
POST/change-passwordPasswort ändern
-
PUT/notificationsUser-Benachrichtigungseinstellungen
-
PUT/staff-notificationsStaff-Benachrichtigungseinstellungen
-
GET/microsoftMicrosoft OAuth-Redirect
-
GET/microsoft/callbackOAuth-Callback
-
GET/microsoft/addin*Outlook-AddIn OAuth Flow
-
-
- - -
-
- USER - Benutzerverwaltung - /api/users -
-
-
GET/Alle User (canViewOnboarding)
-
GET/rolesAlle Rollen
-
GET/audit-logsAudit-Log gesamt
-
GET/audit-logs/user/:userIdAudit-Log nach User
-
GET/export/csvCSV-Export aller User
-
GET/import/azure/groupsAzure-Gruppen (super_admin)
-
POST/import/azureAzure-Import (super_admin)
-
POST/:id/reset-entraPW-Reset via Entra
-
POST/User anlegen (super_admin)
-
PUT/:idUser updaten (super_admin)
-
PUT/:id/roleRolle zuweisen
-
PUT/:id/activateUser aktivieren/deaktivieren
-
DEL/:idUser löschen (super_admin)
-
-
- - -
-
- TICKET - Helpdesk-Tickets - /api/tickets -
-
-
GET/Alle Tickets (canViewTickets)
-
GET/statsTicket-Statistiken
-
GET/metricsSLA-Metriken, Durchlaufzeiten
-
GET/routingRouting-Regeln (Admin)
-
GET/:id/eventsSSE-Stream für Live-Updates
-
GET/:id/feedbackKundenfeedback-Formular (öffentl.)
-
GET/:id/historyTicket-Änderungshistorie
-
GET/:id/pdfTicket als PDF exportieren
-
POST/publicÖffentliches Ticket erstellen (kein Auth)
-
POST/:id/commentsKommentar hinzufügen
-
POST/:id/ai-replyKI-generierte Antwort
-
PUT/bulkBulk-Update mehrerer Tickets
-
PUT/:id/snoozeTicket snoozen
-
-
- - -
-
- ASSET - Hardware-Assets - /api/assets · /api/asset-types -
-
-
GET/Alle Assets
-
GET/mineEigene Assets des eingeloggten Users
-
GET/statsAsset-Statistiken
-
GET/:id/labelQR-Label generieren
-
GET/:id/handover-protocolÜbergabeprotokoll PDF
-
GET/:id/historyZuweisungs-Historie
-
GET/:id/inspectionsPrüf-Protokolle
-
POST/import/intuneIntune-Geräte-Import
-
POST/:id/assignAsset zuweisen
-
POST/:id/sync-agentSync vom Windows-Agenten
-
-
- - -
-
- MON - Windows-Agent-Monitoring - /api/monitoring -
-
-
POST/checkinAgent Check-in (X-Agent-Key, kein JWT)
-
POST/announcements-pollAgent Ankündigungs-Poll
-
GET/agent-scriptAgent-Download (C# v2.0.0)
-
GET/agent-setupInstaller-Download (.exe)
-
GET/statisticsMonitoring-Statistiken (Admin)
-
GET/Alle Monitoring-Geräte (Admin)
-
GET/:idGerät-Details
-
DEL/:idGerät entfernen
-
-
- - -
-
- PATCH - Patch-Management - /api/patch -
-
-
POST/commands/resultAgent meldet Command-Ergebnis (kein JWT)
-
GET/overviewPatch-Übersicht aller Geräte
-
GET/groupsRollout-Gruppen
-
POST/groupsGruppe erstellen
-
POST/policiesUpdate-Richtlinie erstellen/updaten
-
POST/assignAgent einer Gruppe zuweisen
-
POST/groups/:id/releaseVersion an Gruppe freigeben
-
POST/commands/triggerRemote-Command auslösen
-
POST/commands/trigger-groupCommand für ganze Gruppe
-
-
- - -
-
- HR - Onboarding / Offboarding - /api/onboarding · /api/offboarding · /api/onboarding-processes -
-
-
GET/onboarding/confirm/:tokenBestätigung per Token (öffentlich)
-
GET/onboarding/entra-managerEntra-Manager suchen
-
POST/onboarding/:id/send-confirmationBestätigungs-E-Mail senden
-
POST/onboarding/:id/regenerate-pdfPDF neu generieren
-
POST/offboarding/:id/return-assetsAssets zurückgeben
-
GET/onboarding-processes/departmentsAbteilungen für Checklisten
-
GET/onboarding-processes/processesProzess-Vorlagen
-
-
- - -
-
- KI - KI / Claude-Integration - /api/ai · /api/knowledge -
-
-
GET/ai/statusAPI-Verfügbarkeit, Modell-Info
-
POST/ai/chatChat mit Claude (claude-sonnet-4-6)
-
GET/ai/knowledge-baseKI-KB Einträge abrufen
-
POST/ai/knowledge-base/import-textText als KB importieren
-
POST/ai/knowledge-base/import-urlURL scrapen & importieren
-
POST/ai/knowledge-base/import-crawlWebsite crawlen & importieren
-
GET/knowledge/Manuelle KB-Artikel
-
GET/knowledge/category/:catKB nach Kategorie
-
-
- - -
-
- INFRA - Infrastruktur-Monitoring - /api/proxmox · /api/docker · /api/network-monitor -
-
-
GET/proxmox/overviewProxmox VMs, CTs, Node-Stats (gecacht)
-
GET/docker/containersDocker-Container-Liste
-
GET/docker/statsRessourcen-Statistiken
-
GET/docker/containers/:id/logsContainer-Logs
-
POST/docker/containers/:id/:actionStart/Stop/Restart
-
GET/network-monitor/sseSSE Live-Stream für Netzwerk-Status
-
POST/network-monitor/discoverAuto-Discovery via ICMP/SNMP
-
POST/network-monitor/:id/check-nowSofort-Check auslösen
-
GET/network-monitor/uptime-statsUptime-Statistiken aller Geräte
-
-
- - -
-
- SEC - Sicherheit & Compliance - /api/entra · /api/external-alerts · /api/security-reports · /api/scanner -
-
-
GET/entra/usersAzure AD User
-
GET/entra/users/:id/signin-riskAnmelde-Risiko-Score
-
POST/entra/users/:id/invalidate-sessionsSessions widerrufen
-
GET/entra/conditional-access-policiesConditional Access Policies
-
GET/external-alerts/Externe Alerts (Monitoring-Postfach)
-
POST/external-alerts/:id/create-ticketAlert zu Ticket umwandeln
-
POST/security-reports/uploadSicherheitsbericht hochladen
-
POST/scanner/assetsScanner meldet Assets
-
POST/scanner/alertScanner meldet Alert
-
-
- - -
-
- MISC - Diverses - /api/announcements · /api/shares · /api/licenses · /api/warehouse · /api/portal-guides · /api/risks · /api/iso-tasks · /api/it-topics · /api/tv · /api/bot -
-
-
GET/announcements/activeAktive Ankündigungen (Notification Bell)
-
POST/announcements/:id/ack-agentAgent bestätigt Ankündigung (kein JWT)
-
POST/shares/Secure-Share-Link erstellen
-
GET/shares/public/:tokenShare abrufen (öffentlich)
-
GET/warehouse/summaryLager-Übersicht mit Statistiken
-
GET/warehouse/violationsMindestbestand-Verletzungen
-
POST/bot/messagesTeams-Bot Webhook (Adaptive Cards)
-
GET/tv/statsTV-Dashboard Statistiken
-
GET/portal-guides/Anleitungs-Bibliothek
-
POST/it-topics/planner/syncManueller Planner-Sync
-
GET/risks/statsRisiko-Matrix-Statistiken
-
GET/licenses/statisticsLizenz-Auslastung
-
GET/fido-keys/FIDO2-Schlüssel-Inventar
-
GET/unifi/devicesUniFi-Geräte (deakt. Poller)
-
-
-
- - -
-

Empfehlungen

-
- -
-

Sicherheits-Risiken

-
    -
  • TVDashboardPage (/tv) hat keinerlei Auth-Schutz — URL-Kenntnis reicht für Vollzugriff auf Ticket-Stats und Agent-Status
  • -
  • DefectReportPage ist öffentlich — kein CAPTCHA/Rate-Limiting gegen Spam-Tickets
  • -
  • /api/health/history ist public — gibt interne Statushistorie preis
  • -
  • CORS in Produktion: extra PATCH-Methode fehlt (nur GET/POST/PUT/DELETE erlaubt)
  • -
-
- -
-

Altlasten bereinigen

-
    -
  • OnboardingPage.jsx & OffboardingPage.jsx löschen — vollständig durch OnOffboardingPage ersetzt, aber Dateien noch im Repository
  • -
  • NetworkMonitorPage.jsx löschen — Route ist ein Redirect, die Datei ist totes Gewicht
  • -
  • Route /network-monitor entfernen (Redirect ist technisch unnötig wenn Datei weg)
  • -
  • ApiDocsPage manuell gepflegt — veraltet wenn neue Endpoints fehlen; besser auto-generieren (swagger-jsdoc)
  • -
  • unifi.routes.js ist aktiv gemountet, aber Poller deaktiviert — Code-Kommentar in server.js, aber Route aktiv
  • -
-
- -
-

Zusammenfassen / Vereinheitlichen

-
    -
  • UsersPage (/users) + UserManagementPage (/benutzerverwaltung) — prüfen ob Überlappung besteht, ggf. zu einer Seite mergen
  • -
  • KnowledgeBasePage + KnowledgeAiPage — manuelle KB und KI-KB sind getrennte Seiten aber eng verwandt; könnten Tabs in einer Seite sein
  • -
  • DocsPage + ApiDocsPage — beide Admin-only Doku, könnten als Tabs gebündelt werden
  • -
  • IsoPage + RiskPage — beide Compliance-Seiten, könnten unter /compliance gebündelt werden
  • -
  • ProxmoxPage ist derzeit nur eine Übersicht (1 Endpoint) — könnte Tab in MonitoringPage werden
  • -
-
- -
-

Fehlende Features / Erweiterungen

-
    -
  • VPN Site-to-Site Monitoring fehlt noch (Sophos XG API oder ping-basiert) — in Memory als offen markiert
  • -
  • Agent v2.0.0 Features: Software-Inventar, BitLocker-Status, Defender-Status vom Agenten
  • -
  • RiskPage hat keinen PDF-Export (ISO 27001 verlangt Nachweis)
  • -
  • IsoPage hat kein Reporting/Dashboard (nur Checkliste)
  • -
  • SecurityReportsPage ist rein manuell — kein automatischer Report-Import/Trigger
  • -
  • ScannerPage ist ausbaubar: keine automatische Asset-Übernahme ins Asset-Modul
  • -
  • AnlagevermoegenPage fehlt Abschreibungs-Berechnung und Export
  • -
  • Kein globales Audit-Dashboard (nur pro-User in UserPage)
  • -
  • Teams-Tab in EntraPage nicht vollständig (Team.ReadBasic.All Permission fehlt laut CLAUDE.md)
  • -
  • FidoKeysPage könnte Entra-Sync für WebAuthn-Methoden bekommen
  • -
-
- -
-
- - -
-

Cron-Jobs & Background-Services

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ServiceIntervallZweckBedingung
Graph API Mail-Pollingalle 30 Sek.E-Mails abrufen und zu Tickets konvertierenAZURE_TENANT_ID gesetzt
Monitoring-Postfachalle 30 Sek.Netgo/Monitoring-Alerts als External AlertsMONITORING_MAILBOX gesetzt
MDO Alert Pollingalle 5 Min.Microsoft Defender for Office 365 AlertsAZURE_TENANT_ID gesetzt
Planner Auto-Syncalle 10 Sek.IT-Topics mit Microsoft Planner synchronisierenPLANNER_PLAN_ID gesetzt
Eskalations-CheckstündlichÜberfällige Tickets eskalieren, SLA-Benachrichtigungenimmer aktiv
Shares-Cleanupalle 15 Min.Abgelaufene/verbrauchte Share-Links löschenimmer aktiv
Mindestbestand-Checktäglich 07:00Lager-Unterschreitungen per E-Mail meldenimmer aktiv
Health-Historyalle 5 Min.API/DB-Status in health_history aufzeichnenimmer aktiv
Wöchentlicher BerichtMo. 08:00Ticket-Wochen-Report per E-Mailimmer aktiv
Wartungs-Benachrichtigungtäglich 08:00Fällige Asset-Wartungen per E-Mailimmer aktiv
Netzwerk-Monitoring-PollerkonfigurierbarICMP/SNMP-Checks, E-Mail-Alert bei Down/Upimmer aktiv
Teams Channel Pollingalle 10 Min.Neue Teams-Nachrichten/Kanäle erkennenAZURE_TENANT_ID gesetzt
Proxmox Monitoringalle 5 Min.VM/CT-Status von Proxmox REST API pollenPROXMOX_HOST gesetzt
Check-History Cleanuptäglich 03:00Alte Netzwerk-Check-Historien bereinigenimmer aktiv
-
-
- -
- - - - - - -
-
-
-

Asset-Bereich — Detailansicht

-
-

Vollständige technische Referenz: Frontend-Komponenten, API-Endpoints, Datenbankstruktur und Rollen-Matrix

- - -

Architektur-Übersicht

-
- - -
-
-
🖥️
-
-
Frontend
-
React-Komponenten
-
-
-
- AssetsPage.jsx - AssetModal.jsx - AssetAssignModal.jsx - ProduktionAssetWizard.jsx -
-
- - -
-
-
⚙️
-
-
Backend
-
Express REST API
-
-
-
-
20
-
API Endpoints
-
-
- - -
-
-
🗄️
-
-
Datenbank
-
SQLite — assets Tabelle
-
-
-
-
57
-
Felder / Spalten
-
-
- - -
-
-
🔌
-
-
Integrationen
-
Extern & Intern
-
-
-
-
Intune (Import)
-
IT Nexus Agent (Sync)
-
PDF (Label + Übergabe)
-
Audit-Log
-
-
-
- - -

API Endpoints

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodeEndpointBeschreibung
GET/assetsAlle Assets abrufen (gefiltert nach Rollen)
GET/assets/mineEigene zugewiesene Assets des eingeloggten Users
GET/assets/:idEinzelnes Asset mit allen Details
GET/assets/serial/:snAsset per Seriennummer suchen (Scanner-Workflow)
GET/assets/statsDashboard-Statistiken: Gesamtanzahl, Status-Verteilung, AfA-Summen
POST/assetsNeues Asset anlegen
PUT/assets/:idAsset aktualisieren (alle Felder)
DELETE/assets/:idAsset löschen (nur Admin)
POST/assets/:id/assignAsset einem User zuweisen + Übergabe-Log
POST/assets/:id/unassignZuweisung aufheben + Rückgabe-Log
GET/assets/:id/historyZuweisungs- und Änderungshistorie
GET/assets/:id/labelQR-Label als PDF generieren
GET/assets/:id/handover-protocolÜbergabeprotokoll als PDF generieren
GET/assets/:id/inspectionsPrüfberichte / Wartungsberichte abrufen
POST/assets/:id/inspectionsNeuen Prüfbericht erstellen
POST/assets/:id/sync-agentAgent-Daten manuell synchronisieren (OS, IP, Hersteller)
POST/assets/import/intuneMassenimport aus Microsoft Intune via Graph API
-
- - -

Datenbankfelder — assets Tabelle

-
- - -
-
Basis
-
- id - name - type - serial_number - model - description - status -
-
- - -
-
Zuweisung
-
- assigned_to_user_id - created_by_user_id - updated_by_user_id -
-
- - -
-
Kaufdaten / AfA
-
- purchase_date - purchase_price - useful_life_years - residual_value - inventory_number -
-
- - -
-
Wartung
-
- last_maintenance_date - next_maintenance_date - maintenance_interval_months - maintenance_notes -
-
- - -
-
IT / Agent
-
- os - ip_address - manufacturer - teamviewer_id - last_agent_sync -
-
- - -
-
Organisation
-
- department - location_id - min_stock -
-
- - -
-
Audit
-
- created_at - updated_at -
-
-
- - -

Rollen-Matrix

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RolleLesenErstellenBearbeitenZuweisenLöschenBesonderheit
Super Admin / AdminVollzugriff — alle Departments
BearbeiterKein Löschen — alle Departments
TechnikerNur Department Produktion via ProduktionAssetWizard
BenutzerNur 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

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureBeschreibungAufwandPriorität
FernzugriffMeshCentral self-hosted (Docker), API-Link aus IT Nexus direkt in SessionMittel🔴 Hoch
Backup-MonitoringAgent meldet Veeam/Windows Backup Status; IT Nexus zeigt letztes Backup + Erfolg/FehlerKlein🔴 Hoch
Server-MonitoringIT Nexus Agent v2.0.0 auf Windows Servern deployen — Server-spezifische Metriken: Dienste-Status, Eventlog-Fehler, RAID, Windows UpdateKlein🔴 Hoch
Linux Server MonitoringSSH-basiertes Polling (CPU, RAM, Disk, Dienste) direkt aus BackendMittel🟡 Mittel
Health-ScorePro Gerät/Kunde: Gewichteter Score aus Patch-Status, Backup, Online, DefenderGroß⚪ Niedrig
Multi-TenantKundenmandanten trennen — Grundstruktur für MSP-BetriebGroß⚪ 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 - - - - - -
- -
-
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
-
-
- - -
-
- - -
- - - - - -
- - -
-
🎫
-
-
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: OffenIn Bearbeitung
-
01.04.2026 · von Simon Grüßing
-
-
-
Status geändert: In BearbeitungOffen (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 - - - - - - - - - -
- - -
-
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
-
-
- -
- - - - - - - diff --git a/agent/ann-watcher.ps1 b/agent/ann-watcher.ps1 deleted file mode 100644 index eea453e..0000000 --- a/agent/ann-watcher.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -# IT Nexus Announcement Watcher -# Wird alle 15 Sekunden als SYSTEM ausgefuehrt -# Zeigt Ankuendigungen als Desktop-Popup fuer den eingeloggten Benutzer - -param() -$cfg = Get-Content 'C:\ProgramData\IT Nexus Agent\config.json' -Raw | ConvertFrom-Json -$url = $cfg.server_url.TrimEnd('/') -$key = $cfg.agent_key - -function Show-AnnDialog($ann) { - $id = $ann.id - $dlg = "C:\ProgramData\IT Nexus Agent\ann_$id.ps1" - if (Test-Path $dlg) { return } - - $title = ($ann.title -replace "'", "''") - $message = ($ann.message -replace "'", "''") - $accent = switch ($ann.type) { 'warning'{'220,50,50'} 'maintenance'{'245,158,11'} default{'99,102,241'} } - $label = switch ($ann.type) { 'warning'{'WICHTIGE WARNUNG'} 'maintenance'{'WARTUNGSANKUENDIGUNG'} default{'INFORMATION'} } - $ackUrl = "$url/api/announcements/$id/ack-agent" - $n = [System.Environment]::NewLine - - $headerColor = switch ($ann.type) { 'warning'{'220,53,69'} 'maintenance'{'255,140,0'} default{'0,120,212'} } - $iconText = switch ($ann.type) { 'warning'{'⚠'} 'maintenance'{'🔧'} default{'ℹ'} } - - $code = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing$n" - # Hauptfenster - weißer Hintergrund, professionell - $code += "`$f=New-Object System.Windows.Forms.Form;`$f.Text='IT Nexus Mitteilung';`$f.Size=New-Object System.Drawing.Size(480,320);`$f.StartPosition='CenterScreen';`$f.FormBorderStyle='FixedSingle';`$f.MaximizeBox=`$false;`$f.MinimizeBox=`$false;`$f.TopMost=`$true;`$f.BackColor=[System.Drawing.Color]::White$n" - # Farbiger Header-Balken - $code += "`$hdr=New-Object System.Windows.Forms.Panel;`$hdr.Size=New-Object System.Drawing.Size(480,56);`$hdr.Location=New-Object System.Drawing.Point(0,0);`$hdr.BackColor=[System.Drawing.Color]::FromArgb($headerColor);`$f.Controls.Add(`$hdr)$n" - # Logo-Text links im Header - $code += "`$logo=New-Object System.Windows.Forms.Label;`$logo.Text='IT Nexus';`$logo.Font=New-Object System.Drawing.Font('Segoe UI',11,[System.Drawing.FontStyle]::Bold);`$logo.ForeColor=[System.Drawing.Color]::White;`$logo.Location=New-Object System.Drawing.Point(16,8);`$logo.Size=New-Object System.Drawing.Size(120,20);`$hdr.Controls.Add(`$logo)$n" - # Typ-Label im Header - $code += "`$lbl=New-Object System.Windows.Forms.Label;`$lbl.Text='$label';`$lbl.Font=New-Object System.Drawing.Font('Segoe UI',8);`$lbl.ForeColor=[System.Drawing.Color]::FromArgb(220,240,255);`$lbl.Location=New-Object System.Drawing.Point(16,30);`$lbl.Size=New-Object System.Drawing.Size(420,18);`$hdr.Controls.Add(`$lbl)$n" - # Titel - $code += "`$lt=New-Object System.Windows.Forms.Label;`$lt.Text='$title';`$lt.Font=New-Object System.Drawing.Font('Segoe UI',12,[System.Drawing.FontStyle]::Bold);`$lt.ForeColor=[System.Drawing.Color]::FromArgb(30,30,30);`$lt.Location=New-Object System.Drawing.Point(16,72);`$lt.Size=New-Object System.Drawing.Size(444,28);`$f.Controls.Add(`$lt)$n" - # Trennlinie - $code += "`$sep=New-Object System.Windows.Forms.Panel;`$sep.Size=New-Object System.Drawing.Size(448,1);`$sep.Location=New-Object System.Drawing.Point(16,104);`$sep.BackColor=[System.Drawing.Color]::FromArgb(220,220,220);`$f.Controls.Add(`$sep)$n" - # Nachricht - $code += "`$lm=New-Object System.Windows.Forms.Label;`$lm.Text='$message';`$lm.Font=New-Object System.Drawing.Font('Segoe UI',10);`$lm.ForeColor=[System.Drawing.Color]::FromArgb(60,60,60);`$lm.Location=New-Object System.Drawing.Point(16,112);`$lm.Size=New-Object System.Drawing.Size(444,120);`$lm.AutoSize=`$false;`$f.Controls.Add(`$lm)$n" - # Button - $code += "`$btn=New-Object System.Windows.Forms.Button;`$btn.Text='✓ Gelesen und bestätigt';`$btn.Font=New-Object System.Drawing.Font('Segoe UI',10,[System.Drawing.FontStyle]::Bold);`$btn.ForeColor=[System.Drawing.Color]::White;`$btn.BackColor=[System.Drawing.Color]::FromArgb($headerColor);`$btn.FlatStyle='Flat';`$btn.FlatAppearance.BorderSize=0;`$btn.Location=New-Object System.Drawing.Point(16,244);`$btn.Size=New-Object System.Drawing.Size(444,36);`$btn.DialogResult=[System.Windows.Forms.DialogResult]::OK;`$f.Controls.Add(`$btn);`$f.AcceptButton=`$btn$n" - $code += "if(`$f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){$n" - $code += " `$b=[System.Text.Encoding]::UTF8.GetBytes('{""hostname"":""+`$env:COMPUTERNAME+'""}')$n" - $code += " `$r=[System.Net.WebRequest]::Create('$ackUrl')$n" - $code += " `$r.Method='POST';`$r.ContentType='application/json';`$r.Headers.Add('X-Agent-Key','$key');`$r.ContentLength=`$b.Length$n" - $code += " `$s=`$r.GetRequestStream();`$s.Write(`$b,0,`$b.Length);`$s.Close()$n" - $code += " try{`$r.GetResponse().Close()}catch{}$n" - $code += " Remove-Item '$dlg' -Force -ErrorAction SilentlyContinue$n" - $code += "}$n" - - $code | Out-File -FilePath $dlg -Encoding UTF8 -Force - - $loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName - if ($loggedUser) { - $tn = "ITNexus-AnnDlg-$id" - Unregister-ScheduledTask -TaskName $tn -Confirm:$false -ErrorAction SilentlyContinue - $a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$dlg`"" - $t = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2) - $p = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited - $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable - Register-ScheduledTask -TaskName $tn -Action $a -Trigger $t -Principal $p -Settings $s -Force | Out-Null - } -} - -try { - $body = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"'+$env:COMPUTERNAME+'"}') - $req = [System.Net.WebRequest]::Create("$url/api/monitoring/announcements-poll") - $req.Method = 'POST'; $req.ContentType = 'application/json' - $req.Headers.Add('X-Agent-Key', $key); $req.ContentLength = $body.Length - $st = $req.GetRequestStream(); $st.Write($body, 0, $body.Length); $st.Close() - $rd = New-Object System.IO.StreamReader($req.GetResponse().GetResponseStream()) - $data = $rd.ReadToEnd() | ConvertFrom-Json - if ($data.announcements) { foreach ($a in $data.announcements) { Show-AnnDialog $a } } -} catch {} diff --git a/agent/config.example.json b/agent/config.example.json deleted file mode 100644 index e6ff4f0..0000000 --- a/agent/config.example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "server_url": "https://it-nexus.cereda-systems.de", - "agent_key": "DEIN-AGENT-KEY-HIER" -} diff --git a/agent/config.json.template b/agent/config.json.template deleted file mode 100644 index 1e05ac9..0000000 --- a/agent/config.json.template +++ /dev/null @@ -1,4 +0,0 @@ -{ - "server_url": "https://it-nexus.cereda-systems.de", - "agent_key": "cereda-agent-2024-secure-key" -} diff --git a/agent/install.ps1 b/agent/install.ps1 deleted file mode 100644 index d7e8b4a..0000000 --- a/agent/install.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -# IT Nexus Agent - Installer -# Cereda Systems GmbH - -$InstallDir = "$env:ProgramData\IT Nexus Agent" -$AgentKey = "cereda-agent-2024-secure-key" -$ServerUrl = "https://it-nexus.cereda-systems.de" -$TaskName = "IT Nexus Agent" - -Write-Host "=== IT Nexus Agent Installation ===" -ForegroundColor Cyan - -# Verzeichnis erstellen -if (-not (Test-Path $InstallDir)) { - New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null -} - -# Dateien kopieren -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Copy-Item "$ScriptDir\it-nexus-agent.ps1" "$InstallDir\it-nexus-agent.ps1" -Force - -# Config schreiben -$config = @{ server_url = $ServerUrl; agent_key = $AgentKey } | ConvertTo-Json -Set-Content -Path "$InstallDir\config.json" -Value $config -Force - -# Alten Task entfernen falls vorhanden -Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - -# Scheduled Task erstellen (laeuft alle 5 Minuten als SYSTEM) -$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `"$InstallDir\it-nexus-agent.ps1`"" -$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) -Once -At (Get-Date) -$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew -StartWhenAvailable -$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest -ErrorAction SilentlyContinue -if (-not $principal) { - $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -RunLevel Highest -} - -Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null - -Write-Host "Installiert in: $InstallDir" -ForegroundColor Green -Write-Host "Scheduled Task '$TaskName' erstellt (alle 5 Min)" -ForegroundColor Green -Write-Host "Server: $ServerUrl" -ForegroundColor Green -Write-Host "" -Write-Host "Erster Checkin wird jetzt ausgefuehrt..." -ForegroundColor Yellow -Start-ScheduledTask -TaskName $TaskName -Write-Host "Fertig!" -ForegroundColor Green diff --git a/agent/it-nexus-agent.ps1 b/agent/it-nexus-agent.ps1 deleted file mode 100644 index dd193d2..0000000 --- a/agent/it-nexus-agent.ps1 +++ /dev/null @@ -1,606 +0,0 @@ -# IT Nexus Monitoring Agent v1.3 -# Cereda Systems GmbH -# Laedt Systemdaten und sendet sie an IT Nexus - -$AgentVersion = "1.3.1" -$ConfigPath = Join-Path $PSScriptRoot "config.json" -$LogPath = Join-Path $PSScriptRoot "agent.log" - -function Write-Log($msg) { - $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg" - Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue -} - -# Config laden -if (-not (Test-Path $ConfigPath)) { - Write-Log "ERROR: config.json nicht gefunden" - exit 1 -} -$config = Get-Content $ConfigPath | ConvertFrom-Json -$ServerUrl = $config.server_url.TrimEnd('/') -$AgentKey = $config.agent_key - -function Get-CpuUsage { - try { - $pc = [System.Diagnostics.PerformanceCounter]::new("Processor", "% Processor Time", "_Total") - $pc.NextValue() | Out-Null - Start-Sleep -Milliseconds 800 - return [math]::Round($pc.NextValue(), 1) - } catch { return $null } -} - -function Get-RamInfo { - try { - $os = Get-CimInstance Win32_OperatingSystem - $total = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2) - $free = [math]::Round($os.FreePhysicalMemory / 1MB, 2) - return @{ total = $total; used = [math]::Round($total - $free, 2) } - } catch { return @{ total = $null; used = $null } } -} - -function Get-DiskInfo { - try { - $disk = Get-PSDrive C - $total = [math]::Round(($disk.Used + $disk.Free) / 1GB, 2) - $free = [math]::Round($disk.Free / 1GB, 2) - return @{ total = $total; free = $free } - } catch { return @{ total = $null; free = $null } } -} - -function Get-CpuInfo { - try { - $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 - return @{ model = $cpu.Name.Trim(); cores = $cpu.NumberOfCores } - } catch { return @{ model = $null; cores = $null } } -} - -function Get-MacAddress { - try { - $mac = (Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled } | Select-Object -First 1).MACAddress - return $mac - } catch { return $null } -} - -function Get-InstalledSoftware { - try { - $paths = @( - 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' - ) - $apps = Get-ItemProperty $paths -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -and $_.DisplayName -notmatch '^KB\d+' } | - Select-Object -ExpandProperty DisplayName -Unique | - ForEach-Object { [System.Text.RegularExpressions.Regex]::Replace($_, '[^\x20-\x7E]', '') } | - Where-Object { $_.Length -gt 2 } | - Sort-Object | - Select-Object -First 100 - return $apps - } catch { return @() } -} - -function Get-PendingUpdates { - try { - $session = New-Object -ComObject Microsoft.Update.Session - $searcher = $session.CreateUpdateSearcher() - $result = $searcher.Search("IsInstalled=0 and Type='Software'") - return $result.Updates.Count - } catch { return 0 } -} - -function Get-UptimeHours { - try { - $uptime = (Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime - return [math]::Round($uptime.TotalHours, 1) - } catch { return $null } -} - -function Get-LastUser { - try { - $user = (Get-CimInstance Win32_ComputerSystem).UserName - if ($user) { return $user.Split('\')[-1] } - $profile = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' | - Where-Object { $_.ProfileImagePath -like 'C:\Users\*' -and $_.ProfileImagePath -notlike '*default*' } | - Sort-Object { $_.PSChildName } | Select-Object -Last 1 - return ($profile.ProfileImagePath -split '\\')[-1] - } catch { return $null } -} - -function Get-TpmInfo { - try { - $tpm = Get-WmiObject -Namespace "Root\CIMv2\Security\MicrosoftTpm" -Class Win32_Tpm -ErrorAction Stop - if ($tpm) { - $specVer = $tpm.SpecVersion - $isV2 = $specVer -like "*2.0*" - return @{ present = $true; version = $specVer; is_v2 = $isV2 } - } - return @{ present = $false; version = $null; is_v2 = $false } - } catch { - return @{ present = $false; version = $null; is_v2 = $false } - } -} - -function Get-SecureBootStatus { - try { - $sb = Confirm-SecureBootUEFI -ErrorAction Stop - return [bool]$sb - } catch { - return $false - } -} - -function Get-Win11Readiness { - $tpm = Get-TpmInfo - $sb = Get-SecureBootStatus - $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 - $ram = (Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize / 1MB - - $cpuOk = $cpu -and $cpu.NumberOfCores -ge 2 -and $cpu.MaxClockSpeed -ge 1000 - $ramOk = $ram -ge 4 - $ready = $tpm.is_v2 -and $sb -and $cpuOk -and $ramOk - - return @{ - tpm_present = $tpm.present - tpm_version = if ($tpm.version) { $tpm.version } else { $null } - tpm_v2 = $tpm.is_v2 - secure_boot = $sb - win11_ready = $ready - } -} - -# Systemdaten sammeln -Write-Log "Sammle Systemdaten..." - -$cpu = Get-CpuInfo -$ram = Get-RamInfo -$disk = Get-DiskInfo -$cpuPct = Get-CpuUsage -$mac = Get-MacAddress -$updates = Get-PendingUpdates -$uptime = Get-UptimeHours -$lastUser = Get-LastUser -$software = Get-InstalledSoftware -$win11 = Get-Win11Readiness - -$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { - $_.InterfaceAlias -notlike '*Loopback*' -and - $_.InterfaceAlias -notlike '*vEthernet*' -and - $_.InterfaceAlias -notlike '*WSL*' -and - $_.InterfaceAlias -notlike '*Virtual*' -and - $_.IPAddress -notlike '169.*' -and - $_.IPAddress -notlike '172.*' -} | Select-Object -First 1).IPAddress -if (-not $ip) { - $ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { - $_.InterfaceAlias -notlike '*Loopback*' -and - $_.InterfaceAlias -notlike '*vEthernet*' -and - $_.IPAddress -notlike '169.*' - } | Select-Object -First 1).IPAddress -} -$os = Get-CimInstance Win32_OperatingSystem -$domain = $env:USERDOMAIN - -$payload = @{ - hostname = $env:COMPUTERNAME - ip_address = $ip - mac_address = $mac - os_name = $os.Caption - os_version = $os.Version - cpu_model = $cpu.model - cpu_cores = $cpu.cores - cpu_usage_percent = $cpuPct - ram_total_gb = $ram.total - ram_used_gb = $ram.used - disk_total_gb = $disk.total - disk_free_gb = $disk.free - last_user = $lastUser - uptime_hours = $uptime - domain = $domain - agent_version = $AgentVersion - installed_software = $software - windows_updates_pending = $updates - tpm_present = $win11.tpm_present - tpm_version = $win11.tpm_version - tpm_v2 = $win11.tpm_v2 - secure_boot = $win11.secure_boot - win11_ready = $win11.win11_ready -} | ConvertTo-Json -Depth 3 - -# --- An IT Nexus senden --- -function Invoke-PatchCommand($cmd, $cmdId) { - Write-Log "PATCH: Command empfangen: $($cmd) (ID: $cmdId)" - $result = "OK" - try { - switch ($cmd) { - 'check_updates' { - UsoClient.exe StartScan 2>$null - Start-Sleep -Seconds 5 - $result = "Update-Scan gestartet" - Write-Log "PATCH: Update-Scan gestartet" - } - 'install_updates' { - UsoClient.exe StartDownload 2>$null - Start-Sleep -Seconds 3 - UsoClient.exe StartInstall 2>$null - $result = "Update-Installation gestartet" - Write-Log "PATCH: Update-Installation gestartet" - } - 'update_agent' { - Write-Log "UPDATE: Manuelles Agent-Update angefordert..." - try { - $scriptPath = $MyInvocation.MyCommand.Path - $tempPath = "$scriptPath.update" - $updateHeaders = @{ 'X-Agent-Key' = $AgentKey } - Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/agent-script" ` - -Headers $updateHeaders -OutFile $tempPath -TimeoutSec 30 - if ((Get-Item $tempPath -ErrorAction SilentlyContinue).Length -gt 1024) { - Copy-Item -Path $tempPath -Destination $scriptPath -Force - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue - $result = "Agent erfolgreich aktualisiert - neue Version aktiv ab naechstem Run" - Write-Log "UPDATE: Manuelles Update erfolgreich" - } else { - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue - $result = "Fehler: Heruntergeladene Datei ungueltig" - Write-Log "UPDATE ERROR: Datei zu klein" - } - } catch { - $result = "Fehler: $($_.Exception.Message)" - Write-Log "UPDATE ERROR: $result" - } - } - 'upgrade_win11' { - Write-Log "WIN11: Starte Windows 11 Upgrade-Prozess..." - $assistantPath = "C:\ProgramData\IT Nexus Agent\Win11Upgrade.exe" - try { - Write-Log "WIN11: Lade Installation Assistant herunter..." - Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2171764" -OutFile $assistantPath -TimeoutSec 300 - if ((Get-Item $assistantPath -ErrorAction SilentlyContinue).Length -gt 1MB) { - # Als angemeldeter Benutzer ausfuehren damit UI sichtbar ist - $loggedInUser = (Get-CimInstance Win32_ComputerSystem).UserName - if ($loggedInUser -and $loggedInUser -ne '') { - Unregister-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -Confirm:$false -ErrorAction SilentlyContinue - $action = New-ScheduledTaskAction -Execute $assistantPath -Argument "/skipeula /auto upgrade" - $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(20) - $principal = New-ScheduledTaskPrincipal -UserId $loggedInUser -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 3) - Register-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null - Start-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" - $reportBody = @{ command_id = $cmdId; status = 'running'; result = "Windows 11 Upgrade gestartet fuer Benutzer $loggedInUser - Fortschritt sichtbar auf dem Geraet" } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $reportBody -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue - Write-Log "WIN11: Upgrade als $loggedInUser gestartet (Status: running)" - return - } else { - Start-Process -FilePath $assistantPath -ArgumentList "/quietinstall /skipeula /auto upgrade" -NoNewWindow - $reportBody = @{ command_id = $cmdId; status = 'running'; result = "Windows 11 Upgrade gestartet (kein Benutzer angemeldet)" } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $reportBody -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue - Write-Log "WIN11: Upgrade als SYSTEM gestartet (kein User)" - return - } - } else { - $result = "Fehler: Download fehlgeschlagen oder Datei zu klein" - Write-Log "WIN11 ERROR: Download fehlgeschlagen" - } - } catch { - $result = "Fehler: $($_.Exception.Message)" - Write-Log "WIN11 ERROR: $result" - } - } - 'reboot' { - $result = "Neustart wird in 60 Sekunden durchgefuehrt" - Write-Log "PATCH: Neustart geplant" - $reportBody = @{ command_id = $cmdId; status = 'done'; result = $result } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" ` - -Method POST -Body $reportBody ` - -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } ` - -TimeoutSec 10 -ErrorAction SilentlyContinue - Start-Sleep -Seconds 5 - shutdown.exe /r /t 60 /c "IT Nexus Patch Management - Geplanter Neustart" - return - } - } - } catch { - $result = "Fehler: $($_.Exception.Message)" - Write-Log "PATCH ERROR: $result" - } - - try { - $reportBody = @{ command_id = $cmdId; status = 'done'; result = $result } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" ` - -Method POST -Body $reportBody ` - -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } ` - -TimeoutSec 10 - } catch { - Write-Log "PATCH: Ergebnis-Meldung fehlgeschlagen: $($_.Exception.Message)" - } -} - -try { - $headers = @{ - 'Content-Type' = 'application/json' - 'X-Agent-Key' = $AgentKey - } - $cleanPayload = $payload -replace '[\x00-\x1F\x7F]', '' - $response = Invoke-RestMethod -Uri "$ServerUrl/api/monitoring/checkin" ` - -Method POST -Body $cleanPayload -Headers $headers -TimeoutSec 30 - Write-Log "OK: Checkin erfolgreich (Hostname: $env:COMPUTERNAME)" - - # Self-Update pruefen - $serverVersion = $response.agent_version - if ($serverVersion -and $serverVersion -ne $AgentVersion) { - Write-Log "UPDATE: Neue Agent-Version verfuegbar: $serverVersion (aktuell: $AgentVersion)" - try { - $scriptPath = $MyInvocation.MyCommand.Path - $tempPath = "$scriptPath.update" - $updateHeaders = @{ 'X-Agent-Key' = $AgentKey } - Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/agent-script" ` - -Headers $updateHeaders -OutFile $tempPath -TimeoutSec 30 - if ((Get-Item $tempPath).Length -gt 1024) { - Copy-Item -Path $tempPath -Destination $scriptPath -Force - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue - Write-Log "UPDATE: Agent erfolgreich auf Version $serverVersion aktualisiert." - } else { - Remove-Item $tempPath -Force -ErrorAction SilentlyContinue - Write-Log "UPDATE: Heruntergeladene Datei zu klein - Update abgebrochen" - } - } catch { - Write-Log "UPDATE ERROR: $($_.Exception.Message)" - } - } - - # Laufende Commands pruefen (z.B. Win11 Upgrade) - if ($response.running_commands -and $response.running_commands.Count -gt 0) { - foreach ($rc in $response.running_commands) { - if ($rc.command -eq 'upgrade_win11') { - $task = Get-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -ErrorAction SilentlyContinue - $proc = Get-Process -Name "Win11Upgrade" -ErrorAction SilentlyContinue - $stillRunning = ($task -and $task.State -eq 'Running') -or ($proc -ne $null) - if (-not $stillRunning) { - $taskInfo = Get-ScheduledTaskInfo -TaskName "IT Nexus Win11 Upgrade" -ErrorAction SilentlyContinue - $exitCode = if ($taskInfo) { $taskInfo.LastTaskResult } else { 0 } - $doneResult = if ($exitCode -eq 0) { "Windows 11 Upgrade abgeschlossen" } else { "Upgrade beendet (Code: $exitCode)" } - $rb = @{ command_id = $rc.id; status = 'done'; result = $doneResult } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $rb -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue - Write-Log "WIN11: Upgrade abgeschlossen (Code: $exitCode)" - } else { - Write-Log "WIN11: Upgrade laeuft noch..." - } - } - } - } - - # Ankuendigungen anzeigen (als eingeloggter Benutzer via ScheduledTask) - if ($response.announcements -and $response.announcements.Count -gt 0) { - foreach ($ann in $response.announcements) { - try { - $annId = $ann.id - $annTitle = $ann.title -replace "'", "''" - $annMessage = $ann.message -replace "'", "''" - $typeLabel = switch ($ann.type) { - 'warning' { 'WICHTIGE WARNUNG' } - 'maintenance' { 'WARTUNGSANKUENDIGUNG' } - default { 'INFORMATION' } - } - $accentColor = switch ($ann.type) { - 'warning' { '220, 50, 50' } - 'maintenance' { '245, 158, 11' } - default { '99, 102, 241' } - } - - # PS-Script das im Benutzer-Kontext laeuft und den Dialog zeigt - $dialogScript = @" -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -`$form = New-Object System.Windows.Forms.Form -`$form.Text = 'IT Nexus - $typeLabel' -`$form.Size = New-Object System.Drawing.Size(520, 330) -`$form.StartPosition = 'CenterScreen' -`$form.FormBorderStyle = 'FixedDialog' -`$form.MaximizeBox = `$false -`$form.MinimizeBox = `$false -`$form.TopMost = `$true -`$form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 37) -`$lTitle = New-Object System.Windows.Forms.Label -`$lTitle.Text = '$annTitle' -`$lTitle.Font = New-Object System.Drawing.Font('Segoe UI', 13, [System.Drawing.FontStyle]::Bold) -`$lTitle.ForeColor = [System.Drawing.Color]::White -`$lTitle.Location = New-Object System.Drawing.Point(20, 20) -`$lTitle.Size = New-Object System.Drawing.Size(460, 36) -`$form.Controls.Add(`$lTitle) -`$lMsg = New-Object System.Windows.Forms.Label -`$lMsg.Text = '$annMessage' -`$lMsg.Font = New-Object System.Drawing.Font('Segoe UI', 10) -`$lMsg.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 220) -`$lMsg.Location = New-Object System.Drawing.Point(20, 66) -`$lMsg.Size = New-Object System.Drawing.Size(460, 155) -`$lMsg.AutoSize = `$false -`$form.Controls.Add(`$lMsg) -`$lHint = New-Object System.Windows.Forms.Label -`$lHint.Text = 'Bitte lesen und Kenntnisnahme bestaetigen.' -`$lHint.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Italic) -`$lHint.ForeColor = [System.Drawing.Color]::FromArgb(130, 130, 150) -`$lHint.Location = New-Object System.Drawing.Point(20, 228) -`$lHint.Size = New-Object System.Drawing.Size(460, 20) -`$form.Controls.Add(`$lHint) -`$btn = New-Object System.Windows.Forms.Button -`$btn.Text = 'Gelesen und bestaetigt' -`$btn.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold) -`$btn.ForeColor = [System.Drawing.Color]::White -`$btn.BackColor = [System.Drawing.Color]::FromArgb($accentColor) -`$btn.FlatStyle = 'Flat' -`$btn.Location = New-Object System.Drawing.Point(20, 255) -`$btn.Size = New-Object System.Drawing.Size(460, 40) -`$btn.DialogResult = [System.Windows.Forms.DialogResult]::OK -`$form.Controls.Add(`$btn) -`$form.AcceptButton = `$btn -`$r = `$form.ShowDialog() -if (`$r -eq [System.Windows.Forms.DialogResult]::OK) { - `$b = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"' + `$env:COMPUTERNAME + '"}') - `$req = [System.Net.WebRequest]::Create('$ServerUrl/api/announcements/$annId/ack-agent') - `$req.Method = 'POST' - `$req.ContentType = 'application/json' - `$req.Headers.Add('X-Agent-Key','$AgentKey') - `$req.ContentLength = `$b.Length - `$s = `$req.GetRequestStream(); `$s.Write(`$b,0,`$b.Length); `$s.Close() - try { `$req.GetResponse().Close() } catch {} -} -"@ - # Dialog-Script temporaer speichern - $scriptFile = "C:\ProgramData\IT Nexus Agent\ann_$annId.ps1" - $dialogScript | Out-File -FilePath $scriptFile -Encoding UTF8 -Force - - # Eingeloggten Benutzer ermitteln - $loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName - if ($loggedUser -and $loggedUser -ne '') { - $taskName = "ITNexus-Ann-$annId" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Normal -File `"$scriptFile`"" - $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3) - $principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited - $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable - Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null - Write-Log "ANNOUNCEMENT: Dialog fuer Benutzer $loggedUser gestartet - ID $annId" - } else { - Write-Log "ANNOUNCEMENT: Kein Benutzer eingeloggt - Dialog wird beim naechsten Check-in versucht" - } - } catch { - Write-Log "ANNOUNCEMENT ERROR: $($_.Exception.Message)" - } - } - } - - # Pending Patch-Commands verarbeiten - if ($response.commands -and $response.commands.Count -gt 0) { - Write-Log "PATCH: $($response.commands.Count) Command(s) empfangen" - foreach ($cmd in $response.commands) { - Invoke-PatchCommand -cmd $cmd.command -cmdId $cmd.id - } - } -} catch { - $statusCode = $_.Exception.Response.StatusCode.Value__ - Write-Log "ERROR: Checkin fehlgeschlagen - HTTP $statusCode - $($_.Exception.Message)" - try { - $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) - Write-Log "ERROR Detail: $($reader.ReadToEnd())" - } catch {} -} - -# ── Schneller Announcement-Poll (alle 15 Sekunden im Hintergrund) ───────────── -function Invoke-AnnouncementPoll { - try { - $pollBody = @{ hostname = $env:COMPUTERNAME } | ConvertTo-Json - $pollResp = Invoke-RestMethod -Uri "$ServerUrl/api/monitoring/announcements-poll" ` - -Method POST -Body $pollBody ` - -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } ` - -TimeoutSec 10 - if ($pollResp.announcements -and $pollResp.announcements.Count -gt 0) { - foreach ($ann in $pollResp.announcements) { - $annId = $ann.id - $annTitle = $ann.title -replace "'", "''" - $annMessage = $ann.message -replace "'", "''" - $accentColor = switch ($ann.type) { - 'warning' { '220, 50, 50' } - 'maintenance' { '245, 158, 11' } - default { '99, 102, 241' } - } - $typeLabel = switch ($ann.type) { - 'warning' { 'WICHTIGE WARNUNG' } - 'maintenance' { 'WARTUNGSANKUENDIGUNG' } - default { 'INFORMATION' } - } - $dialogScript = @" -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -`$form = New-Object System.Windows.Forms.Form -`$form.Text = 'IT Nexus - $typeLabel' -`$form.Size = New-Object System.Drawing.Size(520, 330) -`$form.StartPosition = 'CenterScreen' -`$form.FormBorderStyle = 'FixedDialog' -`$form.MaximizeBox = `$false -`$form.MinimizeBox = `$false -`$form.TopMost = `$true -`$form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 37) -`$lTitle = New-Object System.Windows.Forms.Label -`$lTitle.Text = '$annTitle' -`$lTitle.Font = New-Object System.Drawing.Font('Segoe UI', 13, [System.Drawing.FontStyle]::Bold) -`$lTitle.ForeColor = [System.Drawing.Color]::White -`$lTitle.Location = New-Object System.Drawing.Point(20, 20) -`$lTitle.Size = New-Object System.Drawing.Size(460, 36) -`$form.Controls.Add(`$lTitle) -`$lMsg = New-Object System.Windows.Forms.Label -`$lMsg.Text = '$annMessage' -`$lMsg.Font = New-Object System.Drawing.Font('Segoe UI', 10) -`$lMsg.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 220) -`$lMsg.Location = New-Object System.Drawing.Point(20, 66) -`$lMsg.Size = New-Object System.Drawing.Size(460, 155) -`$lMsg.AutoSize = `$false -`$form.Controls.Add(`$lMsg) -`$lHint = New-Object System.Windows.Forms.Label -`$lHint.Text = 'Bitte lesen und Kenntnisnahme bestaetigen.' -`$lHint.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Italic) -`$lHint.ForeColor = [System.Drawing.Color]::FromArgb(130, 130, 150) -`$lHint.Location = New-Object System.Drawing.Point(20, 228) -`$lHint.Size = New-Object System.Drawing.Size(460, 20) -`$form.Controls.Add(`$lHint) -`$btn = New-Object System.Windows.Forms.Button -`$btn.Text = 'Gelesen und bestaetigt' -`$btn.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold) -`$btn.ForeColor = [System.Drawing.Color]::White -`$btn.BackColor = [System.Drawing.Color]::FromArgb($accentColor) -`$btn.FlatStyle = 'Flat' -`$btn.Location = New-Object System.Drawing.Point(20, 255) -`$btn.Size = New-Object System.Drawing.Size(460, 40) -`$btn.DialogResult = [System.Windows.Forms.DialogResult]::OK -`$form.Controls.Add(`$btn) -`$form.AcceptButton = `$btn -`$r = `$form.ShowDialog() -if (`$r -eq [System.Windows.Forms.DialogResult]::OK) { - `$b = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"' + `$env:COMPUTERNAME + '"}') - `$req = [System.Net.WebRequest]::Create('$ServerUrl/api/announcements/$annId/ack-agent') - `$req.Method = 'POST'; `$req.ContentType = 'application/json' - `$req.Headers.Add('X-Agent-Key','$AgentKey') - `$req.ContentLength = `$b.Length - `$s = `$req.GetRequestStream(); `$s.Write(`$b,0,`$b.Length); `$s.Close() - try { `$req.GetResponse().Close() } catch {} -} -"@ - $scriptFile = "C:\ProgramData\IT Nexus Agent\ann_$annId.ps1" - $dialogScript | Out-File -FilePath $scriptFile -Encoding UTF8 -Force - $loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName - if ($loggedUser) { - $taskName = "ITNexus-Ann-$annId" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Normal -File `"$scriptFile`"" - $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2) - $principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited - $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable - Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null - Write-Log "ANNOUNCEMENT: Sofort-Dialog gestartet fuer $loggedUser - ID $annId" - } - } - } - } catch { - # Stiller Fehler - nicht kritisch - } -} - -# Announcement-Watcher-Task einrichten (alle 15 Sek, bleibt dauerhaft aktiv) -try { - $watcherName = 'IT Nexus Announcement Watcher' - $watcherScript = 'C:\ProgramData\IT Nexus Agent\ann-watcher.ps1' - - # Watcher-Script vom Server laden (immer aktuell) - Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/ann-watcher" ` - -Headers @{ 'X-Agent-Key' = $AgentKey } ` - -OutFile $watcherScript -TimeoutSec 15 -ErrorAction SilentlyContinue - - # Task registrieren falls noch nicht vorhanden - if (-not (Get-ScheduledTask -TaskName $watcherName -ErrorAction SilentlyContinue)) { - $wAct = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `"$watcherScript`"" - $wTrg = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Seconds 15) -Once -At (Get-Date) - $wSet = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 14) -MultipleInstances IgnoreNew -StartWhenAvailable - $wPri = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - Register-ScheduledTask -TaskName $watcherName -Action $wAct -Trigger $wTrg -Settings $wSet -Principal $wPri -Force | Out-Null - Write-Log "ANNOUNCEMENT: Watcher-Task registriert (alle 15 Sekunden)" - } -} catch { - Write-Log "ANNOUNCEMENT WATCHER ERROR: $($_.Exception.Message)" -} diff --git a/agent/setup.iss b/agent/setup.iss deleted file mode 100644 index 04aedbc..0000000 --- a/agent/setup.iss +++ /dev/null @@ -1,103 +0,0 @@ -#define MyAppName "IT Nexus Agent" -#define MyAppVersion "1.2.4" -#define MyAppPublisher "Cereda Systems GmbH" -#define MyAppURL "https://it-nexus.cereda-systems.de" -#define MyAppExeName "it-nexus-agent.ps1" -#define InstallDir "{commonappdata}\IT Nexus Agent" - -[Setup] -AppId={{B3F7A2C1-4E8D-4F2A-9B1C-7D3E5F6A8B2C} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -DefaultDirName={#InstallDir} -DisableDirPage=yes -DefaultGroupName={#MyAppName} -DisableProgramGroupPage=yes -OutputDir=..\dist -OutputBaseFilename=IT-Nexus-Agent-Setup-v{#MyAppVersion} -SetupIconFile= -Compression=lzma2/ultra64 -SolidCompression=yes -WizardStyle=modern -WizardSizePercent=120 -PrivilegesRequired=admin -UninstallDisplayName={#MyAppName} -UninstallDisplayIcon={app}\it-nexus-agent.ps1 -CloseApplications=no -DisableWelcomePage=no -WizardImageFile=compiler:WizModernImage.bmp -WizardSmallImageFile=compiler:WizModernSmallImage.bmp - -[Languages] -Name: "german"; MessagesFile: "compiler:Languages\German.isl" - -[Files] -Source: "it-nexus-agent.ps1"; DestDir: "{app}"; Flags: ignoreversion -Source: "config.json.template"; DestDir: "{app}"; DestName: "config.json"; Flags: ignoreversion onlyifdoesntexist - -[Run] -Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -NonInteractive -Command ""Unregister-ScheduledTask -TaskName 'IT Nexus Agent' -Confirm:$false -ErrorAction SilentlyContinue; $a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File \""{app}\it-nexus-agent.ps1\""'; $t = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 1) -Once -At (Get-Date); $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew -StartWhenAvailable; $p = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest; Register-ScheduledTask -TaskName 'IT Nexus Agent' -Action $a -Trigger $t -Settings $s -Principal $p -Force; Start-ScheduledTask -TaskName 'IT Nexus Agent'"""; Flags: runhidden waituntilterminated; StatusMsg: "Registriere Windows Scheduled Task..." - -[UninstallRun] -Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -NonInteractive -Command ""Unregister-ScheduledTask -TaskName 'IT Nexus Agent' -Confirm:$false -ErrorAction SilentlyContinue"""; Flags: runhidden waituntilterminated - -[Code] -var - ServerUrlPage: TInputQueryWizardPage; - AgentKeyPage: TInputQueryWizardPage; - -procedure InitializeWizard; -begin - ServerUrlPage := CreateInputQueryPage(wpWelcome, - 'Server-Konfiguration', - 'Gib die IT Nexus Server-Adresse ein.', - ''); - ServerUrlPage.Add('Server URL:', False); - ServerUrlPage.Values[0] := 'https://it-nexus.cereda-systems.de'; - - AgentKeyPage := CreateInputQueryPage(ServerUrlPage.ID, - 'API Key', - 'Gib den Agent API Key ein. Diesen findest du in den IT Nexus Einstellungen.', - ''); - AgentKeyPage.Add('Agent API Key:', False); - AgentKeyPage.Values[0] := 'itx-4CPJPTHmCfdrL9D62WacCATEvuvULXcp7ECMpSaNUjsS344F6_L4Ug'; -end; - -function NextButtonClick(CurPageID: Integer): Boolean; -begin - Result := True; - if CurPageID = ServerUrlPage.ID then begin - if ServerUrlPage.Values[0] = '' then begin - MsgBox('Bitte gib eine Server URL ein.', mbError, MB_OK); - Result := False; - end; - end; - if CurPageID = AgentKeyPage.ID then begin - if AgentKeyPage.Values[0] = '' then begin - MsgBox('Bitte gib einen API Key ein.', mbError, MB_OK); - Result := False; - end; - end; -end; - -procedure CurStepChanged(CurStep: TSetupStep); -var - ConfigFile: string; - Config: TStringList; -begin - if CurStep = ssPostInstall then begin - ConfigFile := ExpandConstant('{app}\config.json'); - Config := TStringList.Create; - try - Config.Add('{'); - Config.Add(' "server_url": "' + ServerUrlPage.Values[0] + '",'); - Config.Add(' "agent_key": "' + AgentKeyPage.Values[0] + '"'); - Config.Add('}'); - Config.SaveToFile(ConfigFile); - finally - Config.Free; - end; - end; -end; diff --git a/agent/uninstall.ps1 b/agent/uninstall.ps1 deleted file mode 100644 index 7a4ad88..0000000 --- a/agent/uninstall.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# IT Nexus Agent - Uninstaller -$InstallDir = "C:\Program Files\IT Nexus Agent" -$TaskName = "IT Nexus Agent" - -Write-Host "=== IT Nexus Agent Deinstallation ===" -ForegroundColor Yellow -Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue -Remove-Item -Path $InstallDir -Recurse -Force -ErrorAction SilentlyContinue -Write-Host "Agent entfernt." -ForegroundColor Green diff --git a/demo-benutzerverwaltung.html b/demo-benutzerverwaltung.html deleted file mode 100644 index 40fbce0..0000000 --- a/demo-benutzerverwaltung.html +++ /dev/null @@ -1,749 +0,0 @@ - - - - - -Benutzerverwaltung – Design Demo - - - - - - - -
- Design-Option: -
- - - - -
-
- - -
- - -
- Option A: Statistik-Kacheln oben + Filter-Chips statt nur Textsuche. Tabelle bleibt gleich. Kleinster Aufwand, sofortiger Informationsgewinn. -
- - -
-
-
👥
-
14
Benutzer gesamt
-
-
-
-
11
Aktiv
-
-
-
🚫
-
3
Deaktiviert
-
-
-
⏱️
-
4
Noch nie eingeloggt
-
-
- - -
- -
-
- Alle Rollen - 👑 Super Admin - 🛡️ Admin - 🎧 Support - 🔧 Bearbeiter - 👤 Benutzer -
-
- ● Aktiv - ● Inaktiv -
- - -
- - - - - - - - - - - - - -
BenutzernameE-MailNameRolleStatusLetzter LoginAktionen
-
-
- - -
- - -
- 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. -
- -
- -
- -
- - - - - - - - - - - - -
BenutzerE-MailRolleStatusLetzter Login···
-
-
- - -
- - -
- Option C: Klick auf eine Zeile öffnet ein Detailpanel rechts. Tabelle bleibt links sichtbar. Kein Modal-Overlay. → Klick auf eine Zeile zum Testen! -
- -
- -
- -
-
- - - - - - - - - - - -
BenutzerE-MailRolleStatusLetzter Login
-
- -
-
- - -
- - -
- 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 -
- -
-
- - - - - - - - - - - - -
BenutzerE-MailRolleStatusLetzter 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

-
-
-
- -
- - - - -
-
-
🚀
-

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"}} - - -{{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
-
-
- -
- - - - - - - - - - - - {{range .Computers}} - - - - - - - - {{else}} - - {{end}} - -
Computer-NameAbteilung / OUBetriebssystemLetzter Login (AD)Im Netzwerk
{{.CN}}{{if .Department}}{{.Department}}{{else}}{{end}}{{if .OS}}{{.OS}}{{else}}{{end}} - {{if not .LastLogon.IsZero}}{{formatTime .LastLogon}}{{else}}—{{end}} - - {{if .SeenInNetwork}} - Online - {{else}} - Nicht gesehen - {{end}} -
- Noch keine AD-Daten — AD Sync ausführen. -
-
-{{end}} diff --git a/nexus-scanner/internal/web/templates/assets.html b/nexus-scanner/internal/web/templates/assets.html deleted file mode 100644 index 76baa29..0000000 --- a/nexus-scanner/internal/web/templates/assets.html +++ /dev/null @@ -1,126 +0,0 @@ -{{define "content"}} - - - -
-
-
-
- - -
-
- Standort: -
- - - -
-
-
- Status: -
- - - -
-
- - -
-
-
- - -
- - - - - - - - - - - - - - - {{range .Hosts}} - - - - - - - - - - - {{else}} - - {{end}} - -
IP-AdresseMAC-AdresseHerstellerHostnameOffene PortsStandortZuletzt gesehenStatus
{{.IP}}{{.MAC}} -
-
- - - -
- {{if .Vendor}}{{.Vendor}}{{else}}{{end}} -
-
{{if .Hostname}}{{.Hostname}}{{else}}{{end}} - {{if .OpenPorts}} -
- {{range .OpenPorts}}{{.}}{{end}} -
- {{else}}{{end}} -
{{.Site}}{{.LastSeenFmt}} - {{if eq .Status "online"}} - Online - {{else}} - Offline - {{end}} -
- Keine Geräte gefunden. -
- - {{if gt .TotalPages 1}} -
- - Zeige {{if gt .TotalHosts 0}}{{inc (mul (dec .PageNum) 20)}}–{{end}}{{.TotalHosts}} Einträge - - -
- {{end}} -
-{{end}} diff --git a/nexus-scanner/internal/web/templates/dashboard.html b/nexus-scanner/internal/web/templates/dashboard.html deleted file mode 100644 index 5ee56b9..0000000 --- a/nexus-scanner/internal/web/templates/dashboard.html +++ /dev/null @@ -1,166 +0,0 @@ -{{define "content"}} - - - -
-
-
- - Gefundene Geräte -
-
{{.HostCount}}
-
+{{.RecentCount}} seit letztem Scan
-
- -
-
-
-
- - Neu seit letztem Scan -
-
{{.RecentCount}}
-
In den letzten 60 Min
-
- -
-
-
-
- - Offline-Geräte -
-
{{.OfflineCount}}
-
{{if gt .OfflineCount 5}}Erhöht ggü. Vorwoche{{else}}Im Normalbereich{{end}}
-
- -
-
-
-
- - Letzter Scan -
-
{{.LastScan}}
-
Intervall: {{.ARPInterval}}
-
-
- - -
- -
-
-
-
Zuletzt gefundene Geräte
-
Die 5 jüngsten Einträge im Inventar
-
- - Alle anzeigen - - -
- - - - - - - - - - - - {{range .RecentHosts}} - - - - - - - - {{else}} - - {{end}} - -
GerätIP-AdresseStandortStatusGesehen
-
-
- -
-
-
{{if .Hostname}}{{.Hostname}}{{else}}{{.IP}}{{end}}
-
{{if .Vendor}}{{.Vendor}}{{else}}Unbekannt{{end}}
-
-
-
{{.IP}}{{.Site}} - {{if eq .Status "online"}} - Online - {{else}} - Offline - {{end}} - {{.LastSeenFmt}}
- Noch keine Geräte — Scan starten. -
-
- - -
-
-
-
Aktive Module
-
Status der Scan-Quellen
-
- - Verwalten - - -
- {{range $i, $m := .Modules}} -
-
- - {{if eq $m.Name "arp_discovery"}} - {{else if eq $m.Name "dns_reverse"}} - {{else if eq $m.Name "ad_sync"}} - {{else if eq $m.Name "site_monitoring"}} - {{else if eq $m.Name "port_scan"}} - {{else if eq $m.Name "snmp"}} - {{else}}{{end}} - -
-
-
{{$m.DisplayName}}
-
{{if $m.LastRun}}Letzter Lauf {{$m.LastRun}}{{else}}Noch nicht gelaufen{{end}}
-
- {{if $m.Enabled}}OK - {{else}}Aus{{end}} -
- {{end}} -
-
-{{end}} diff --git a/nexus-scanner/internal/web/templates/layout.html b/nexus-scanner/internal/web/templates/layout.html deleted file mode 100644 index 7e9a5ec..0000000 --- a/nexus-scanner/internal/web/templates/layout.html +++ /dev/null @@ -1,145 +0,0 @@ -{{define "layout"}} - - - - - - {{.Title}} — Nexus Scanner - - - - - -
- - - - - -
-
- -
- {{if .Scanning}} -
Scan läuft…
- {{else}} -
Online · {{.SiteID}}
- {{end}} - - - -
-
- -
- {{template "content" .}} -
-
-
- - - - -{{end}} diff --git a/nexus-scanner/internal/web/templates/login.html b/nexus-scanner/internal/web/templates/login.html deleted file mode 100644 index 71116e6..0000000 --- a/nexus-scanner/internal/web/templates/login.html +++ /dev/null @@ -1,71 +0,0 @@ -{{define "login"}} - - - - - - Anmelden — Nexus Scanner - - - - -
-
- - -

Willkommen zurück.

-

Passwort eingeben um fortzufahren.

- - {{if .Error}} -
{{.Error}}
- {{end}} - -
-
- -
- - -
-
- -
- -

- Nexus Scanner · Cereda Systems GmbH -

-
-
- - - -{{end}} diff --git a/nexus-scanner/internal/web/templates/logs.html b/nexus-scanner/internal/web/templates/logs.html deleted file mode 100644 index 32be253..0000000 --- a/nexus-scanner/internal/web/templates/logs.html +++ /dev/null @@ -1,57 +0,0 @@ -{{define "content"}} - - -
- {{template "log-rows" .}} -
- -
- Aktualisiert automatisch alle 2 Sekunden - · {{len .Logs}} Einträge -
- - -{{end}} - -{{define "log-rows"}} -{{range .Logs}} -
- {{.Time}} - {{.Level}} - {{.Message}} -
-{{else}} -
- Noch keine Log-Einträge. -
-{{end}} -{{end}} diff --git a/nexus-scanner/internal/web/templates/modules.html b/nexus-scanner/internal/web/templates/modules.html deleted file mode 100644 index 8f3f124..0000000 --- a/nexus-scanner/internal/web/templates/modules.html +++ /dev/null @@ -1,69 +0,0 @@ -{{define "content"}} - - -
- {{range .Modules}} -
-
- - - -
- -
-
{{.DisplayName}}
-
{{.Description}} · alle {{.Interval}}
-
- -
- {{if .LastRun}} - Letzter Lauf: {{.LastRun}} - {{end}} - {{if .Enabled}} - Aktiv - {{else}} - Inaktiv - {{end}} -
- -
-
-
- {{else}} -
- Keine Module registriert. Konfiguriere Module in config.yaml. -
- {{end}} -
- -
-
- ARP-Discovery Konfiguration -
-
-
- - {{range .ARPSubnets}} - - {{else}} - - {{end}} -
-
- - -
-
- -
-
-
-{{end}} diff --git a/nexus-scanner/internal/web/templates/monitoring.html b/nexus-scanner/internal/web/templates/monitoring.html deleted file mode 100644 index 409ebf6..0000000 --- a/nexus-scanner/internal/web/templates/monitoring.html +++ /dev/null @@ -1,158 +0,0 @@ -{{define "content"}} - - - -
-
-
Checks gesamt
-
{{.TotalChecks}}
-
konfiguriert
-
-
-
Online
-
{{.OnlineCount}}
-
erreichbar
-
-
-
Offline
-
{{.OfflineCount}}
-
nicht erreichbar
-
-
- - -
- {{if .Checks}} - - - - - - - - - - - - - - - {{range .Checks}} - - - - - - - - - - - {{end}} - -
NameTypZielStatusLatenzVerlauf (letzte 20)Geprüft
{{.Check.Name}} - {{.Check.Type}} - {{.Check.Target}} - {{if not .HasResult}} - Ausstehend - {{else if eq .Latest.Status "online"}} - Online - {{else}} - Offline - {{end}} - - {{if and .HasResult (gt .Latest.LatencyMS 0)}}{{.Latest.LatencyMS}} ms{{else}}—{{end}} - -
- {{range .History}} -
-
- {{end}} - {{if eq (len .History) 0}} - noch keine Daten - {{end}} -
-
- {{if .HasResult}}{{formatTime .Latest.CheckedAt}}{{else}}—{{end}} - -
- -
-
- {{else}} -
-
📡
-
Noch keine Checks konfiguriert
-
Füge unten deinen ersten Check hinzu.
-
- {{end}} -
- - -
-
-
-
Check hinzufügen
-
Jeder Standort prüft unabhängig — Ping, HTTP oder TCP Port.
-
-
- {{if .AddErr}}
{{.AddErr}}
{{end}} -
-
-
- - -
-
- - -
-
- - -
-
-
- - Ping: IP-Adresse    - HTTP/S: https://hostname    - TCP: hostname:port - - -
-
-
-{{end}} diff --git a/nexus-scanner/internal/web/templates/settings.html b/nexus-scanner/internal/web/templates/settings.html deleted file mode 100644 index b163923..0000000 --- a/nexus-scanner/internal/web/templates/settings.html +++ /dev/null @@ -1,252 +0,0 @@ -{{define "content"}} - - -{{if .Saved}}
Einstellungen gespeichert.
{{end}} -{{if .SaveErr}}
{{.SaveErr}}
{{end}} - -
- - -
-
Standort
-
-
-
-
Standort-ID
-
Wird in allen gemeldeten Assets verwendet.
-
-
- -
-
-
-
- - -
-
IT Nexus Integration
-
-
-
- - -
-
- -
- - -
-
-
-
-
- - -
-
Scanner-Optionen
-
-
-
-
Scan-Intervall
-
Wie oft der ARP-Scan automatisch ausgeführt wird.
-
-
- -
-
-
-
- - -
-
Scanner-Verhalten
-
-
-
-
Offline-Schwellwert
-
Host gilt als Offline wenn er seit dieser Zeit nicht mehr gesehen wurde.
-
-
- -
-
-
-
- - -
-
Active Directory
-
-
-
-
AD Sync aktivieren
-
Computer-Objekte aus winkel.local importieren.
-
-
- -
-
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
Benachrichtigungen
-
-
-
-
IT Nexus Alert
-
Bei Ausfall an IT Nexus API melden (POST /api/scanner/alert).
-
-
- -
-
-
-
-
E-Mail (SMTP)
-
E-Mail bei Status-Wechsel senden.
-
-
- -
-
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
Passwort ändern
-
-
-
- - -
-
- - -
-
-
-
- -
- - -
-
- - -{{end}} diff --git a/nexus-scanner/internal/web/templates/setup.html b/nexus-scanner/internal/web/templates/setup.html deleted file mode 100644 index 1d0921d..0000000 --- a/nexus-scanner/internal/web/templates/setup.html +++ /dev/null @@ -1,219 +0,0 @@ -{{define "setup"}} - - - - - - Einrichtung — Nexus Scanner - - - - -
-
- - -
-
-
- -
- Nexus Scanner -
-
- {{range $i, $_ := .Steps}} -
- {{end}} -
-
- - {{if eq .Step 0}} - -
-
- -
-

Willkommen beim Nexus Scanner.

-

Wähle zuerst deinen Standort.

-
- -
- - -
-
-
- - - {{else if eq .Step 1}} - -
-
- -
-

Netzwerk konfigurieren.

-

Welches Subnetz soll gescannt werden?

- {{if .Error}}
{{.Error}}
{{end}} -
- - -
- - -

Beispiel: 192.168.0.0/24 scannt 254 Adressen.

-
-
- - -

Automatisch erkannt — wähle das Interface zum LAN.

-
-
-
- - - {{else if eq .Step 2}} - -
-
- -
-

IT Nexus verbinden.

-

Optional — kann auch später konfiguriert werden.

-
- - - - -
- - -
-
- - -
-
-
- - - {{else if eq .Step 3}} - -
-
- -
-

Passwort festlegen.

-

Mindestens 6 Zeichen für den Web-Login.

- {{if .Error}}
{{.Error}}
{{end}} -
- - - - - - -
- - -
-
- - -
-
-
- - - {{else}} - -
-
- -
-

Alles bereit.

-

Der Scanner ist konfiguriert und startet jetzt.

-
-
{{.Site}}Standort
-
{{.Subnet}}Subnetz
- {{if .NexusURL}}
VerbundenIT Nexus API
{{end}} -
Passwort gesetzt
-
-
- - - - - - - -
-
- - {{end}} - -
-
- - - - -{{end}} diff --git a/nexus-scanner/internal/web/templates/snmp.html b/nexus-scanner/internal/web/templates/snmp.html deleted file mode 100644 index 9a9deb7..0000000 --- a/nexus-scanner/internal/web/templates/snmp.html +++ /dev/null @@ -1,232 +0,0 @@ -{{define "content"}} - - -{{if .AddErr}}
{{.AddErr}}
{{end}} - -{{if .Targets}} -
- {{range .Targets}} -
- -
-
-
- {{if eq .Target.Type "printer"}} - - {{else if eq .Target.Type "switch"}} - - {{else}} - - {{end}} -
-
-
{{.Target.Name}}
-
{{.Target.IP}}
-
-
-
- {{if .HasData}} - Online - {{else}} - Kein Datum - {{end}} -
- -
-
-
- - -
- {{if .HasData}} - {{if .SysName}}
Name
{{.SysName}}
{{end}} - {{if .SysDescr}}
Beschreibung
{{.SysDescr}}
{{end}} - {{if .SysUpTime}}
Uptime
{{.SysUpTime}}
{{end}} - {{if .SysLocation}}
Standort
{{.SysLocation}}
{{end}} - - {{if eq .Target.Type "switch"}} - -
-
- {{if .PortsTotal}} -
-
Ports
-
{{.PortsUp}} / {{.PortsTotal}}
-
● aktiv
-
- {{end}} - {{if .TrafficIn}} -
-
Traffic
-
↓ {{.TrafficIn}}
-
↑ {{.TrafficOut}}
-
- {{end}} - {{if .MemTotal}} -
-
RAM
-
{{.MemUsed}} / {{.MemTotal}}
-
-
-
-
- {{end}} - {{if .CpuLoad}} -
-
CPU
-
{{.CpuLoad}}%
-
-
-
-
- {{end}} -
-
- {{end}} - - {{if eq .Target.Type "printer"}} -
- {{if .PrinterStatus}} -
- - {{.PrinterStatus}} -
- {{end}} - {{if .TonerLevel}} -
- Toner - {{.TonerPct}}% -
-
-
-
- {{if lt .TonerPct 20}}
⚠ Toner bald leer — bitte nachbestellen
{{end}} - {{end}} - {{if .PageCount}} -
- - Gesamtseiten: {{.PageCount}} -
- {{end}} -
- {{end}} - {{else}} -
Noch kein Scan — läuft alle {{.Interval}}.
- {{end}} -
- - -
- Community: {{.Target.Community}} · {{.Target.Version}} -
- {{if .HasData}} - - {{end}} - {{.Target.Type}} -
-
-
- - - {{if .HasData}} - - {{end}} - {{end}} -
-{{else}} -
-
-
📡
-
Noch keine SNMP-Geräte
-
Füge unten dein erstes Gerät hinzu.
-
-
-{{end}} - - -
-
-
SNMP-Gerät hinzufügen
-
Drucker (Kyocera), Switches, USVs — Community string "public" ist Standard.
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-
-
- - -{{end}} diff --git a/nexus-scanner/nexus-scanner-dev.exe~ b/nexus-scanner/nexus-scanner-dev.exe~ deleted file mode 100644 index 8c8e201..0000000 Binary files a/nexus-scanner/nexus-scanner-dev.exe~ and /dev/null differ diff --git a/nexus-scanner/nexus-scanner-linux-amd64 b/nexus-scanner/nexus-scanner-linux-amd64 deleted file mode 100644 index 482d561..0000000 Binary files a/nexus-scanner/nexus-scanner-linux-amd64 and /dev/null differ diff --git a/nexus-scanner/nexus-scanner.exe~ b/nexus-scanner/nexus-scanner.exe~ deleted file mode 100644 index 627df41..0000000 Binary files a/nexus-scanner/nexus-scanner.exe~ and /dev/null differ diff --git a/nexus-scanner/nexus-scanner.service b/nexus-scanner/nexus-scanner.service deleted file mode 100644 index 80bc555..0000000 --- a/nexus-scanner/nexus-scanner.service +++ /dev/null @@ -1,32 +0,0 @@ -[Unit] -Description=Nexus Network Scanner Agent -Documentation=https://it-nexus.cereda-systems.de -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -ExecStart=/usr/local/bin/nexus-scanner /etc/nexus-scanner/config.yaml -Restart=on-failure -RestartSec=10s -User=nexus-scanner -Group=nexus-scanner - -# Allow raw socket access for ARP (runs as non-root). -AmbientCapabilities=CAP_NET_RAW -CapabilityBoundingSet=CAP_NET_RAW - -# Hardening -NoNewPrivileges=yes -ProtectSystem=strict -ProtectHome=yes -PrivateTmp=yes -ReadWritePaths=/var/lib/nexus-scanner - -# Logging — captured by journald. -StandardOutput=journal -StandardError=journal -SyslogIdentifier=nexus-scanner - -[Install] -WantedBy=multi-user.target diff --git a/onboarding_prozess.html b/onboarding_prozess.html deleted file mode 100644 index 897b1f2..0000000 --- a/onboarding_prozess.html +++ /dev/null @@ -1,881 +0,0 @@ - - - - - -Onboarding Prozess – Cereda Systems - - - - - -
-
- CEREDA SYSTEMS -
-

Mitarbeiter Onboarding – Prozessablauf

-

Von Vertragsunterzeichnung bis zum ersten Arbeitstag · Alle Abteilungen

-
-
-
HR / Personal
-
IT
-
Vorgesetzter
-
Buchhaltung / Lohn
-
-
-
- -
- - -
- 📄 - TRIGGER - Arbeitsvertrag unterzeichnet - - Onboarding-Prozess wird gestartet · Alle Abteilungen werden parallel angestoßen - Ziel: Tag 1 = ✅ alles bereit -
- - -

Phase 1 — Sofort nach Vertragsunterzeichnung

- -
-
⏱ Zeitpunkt
-
🧑‍💼 HR / Personal
-
💻 IT
-
👔 Vorgesetzter
-
💶 Buchhaltung / Lohn
-
-
-
-
-
Tag 0
-
Vertrag
unterzeichnet
-
-
-
-
Personalakte anlegen
-
Digitale Akte erstellen · Vertragskopie ablegen · Eintrittsdatum eintragen
- 🔴 Sofort -
-
-
Abteilungen informieren
-
IT, Vorgesetzten & Buchhaltung per E-Mail/Ticket anstoßen mit: Name, Position, Startdatum, Abteilung
- 🔴 Sofort -
-
-
Berufsgenossenschaft anmelden
-
BG-Anmeldung vorbereiten
- Standard -
-
-
-
-
IT-Onboarding-Ticket anlegen
-
Ticket im Helpdesk erstellen mit allen Infos. Startet alle IT-Aufgaben.
- 🔴 Sofort -
-
-
Hardware prüfen & bestellen
-
Verfügbaren Laptop/PC prüfen · ggf. Neubestellung auslösen (Lieferzeit beachten!)
- 🔴 Sofort -
-
-
-
-
Buddy / Paten benennen
-
Einen erfahrenen Kollegen als Begleitung für die ersten Wochen auswählen & informieren
- 🔴 Sofort -
-
-
Einarbeitungsplan erstellen
-
30-Tage-Plan ausarbeiten: Aufgaben, Ziele, erste Projekte, Schulungsbedarf
- ⚡ Diese Woche -
-
-
-
-
Mitarbeiter in DATEV anlegen
-
Stammdaten anlegen · Eintrittsdatum · Kostenstelle · Abteilung
- 🔴 Sofort -
-
-
Bankdaten & Steuer anfordern
-
IBAN-Formular an MA senden · Steuerklasse erfragen · SV-Nummer anfordern
- ⚡ Diese Woche -
-
-
-
- - -

Phase 2 — 2 Wochen vor Arbeitsbeginn

- -
-
⏱ Zeitpunkt
-
🧑‍💼 HR / Personal
-
💻 IT
-
👔 Vorgesetzter
-
💶 Buchhaltung / Lohn
-
-
-
-
-
−14
-
2 Wochen
vor Start
-
-
-
-
Willkommens-E-Mail senden
-
Startzeit, Ansprechpartner, Parkplatz, Dresscode, was mitbringen (Ausweis!)
- ⚡ Wichtig -
-
-
Formulare vorbereiten
-
Datenschutzerklärung · IT-Nutzungsordnung · Betriebsordnung · Schweigepflicht
- Standard -
-
-
Mitarbeiterausweis beantragen
-
Foto anfordern oder Fototermin Tag 1 planen
- Standard -
-
-
-
-
AD-Account erstellen
-
Benutzername nach Konvention · Temporäres Passwort · Gruppe / Abteilung zuordnen
- 🔴 Kritisch -
-
-
E-Mail-Adresse einrichten
-
Postfach anlegen · Signatur vorkonfigurieren · In Verteiler aufnehmen
- 🔴 Kritisch -
-
-
Hardware aufsetzen
-
OS-Image · Windows Updates · Antivirus · Endpoint-Agent · Gerät im Asset-Management erfassen
- ⚡ Wichtig -
-
-
-
-
Team informieren
-
Teammitglieder über neuen Kollegen informieren · Buddy briefen · Mittagessen Tag 1 planen
- ⚡ Wichtig -
-
-
Arbeitsplatz vorbereiten
-
Schreibtisch · Stuhl · Namensschild · Willkommensmappe am Platz bereitstellen
- Standard -
-
-
-
-
Lohnkonto einrichten
-
Gehaltsgruppe · Steuerklasse · Krankenkasse · Urlaubsanspruch in System hinterlegen
- ⚡ Wichtig -
-
-
Sozialversicherung anmelden
-
SV-Anmeldung bei Krankenkasse einreichen
- Standard -
-
-
-
- - -

Phase 3 — 1 Woche vor Arbeitsbeginn

- -
-
⏱ Zeitpunkt
-
🧑‍💼 HR / Personal
-
💻 IT
-
👔 Vorgesetzter
-
💶 Buchhaltung / Lohn
-
-
-
-
-
−7
-
1 Woche
vor Start
-
-
-
-
Vollständigkeits-Check
-
Alle Dokumente vollständig? IBAN erhalten? SV-Nummer? Pflichtunterweisungen geplant?
- 🔴 Pflicht -
-
-
Pflichtunterweisungen planen
-
Termine für Arbeitssicherheit · Brandschutz · Datenschutz in KW 1 einplanen
- ⚡ Wichtig -
-
-
-
-
Kompletttest aller Systeme
-
Login testen · E-Mail senden/empfangen · VPN verbinden · Netzlaufwerke prüfen · Teams testen
- 🔴 Pflicht -
-
-
Software installieren
-
Office 365 · VPN-Client · Passwortmanager · Teams · ERP/CRM · abteilungsspezifische Tools
- 🔴 Pflicht -
-
-
Telefon / Durchwahl einrichten
-
IP-Telefon oder Softphone · Durchwahl zuweisen · Mailbox-Ansage konfigurieren
- ⚡ Wichtig -
-
-
IT-Begrüßungsmappe drucken
-
Quick-Start-Guide: Login, Helpdesk-Kontakt, WLAN, Drucker, wichtige URLs
- Standard -
-
-
-
-
Erste Aufgaben vorbereiten
-
2–3 konkrete, überschaubare Aufgaben für Tag 1 und Woche 1 definieren
- ⚡ Wichtig -
-
-
Check-in-Termine planen
-
Tägliche 15-Min-Calls Woche 1 · Wöchentliche 30-Min-Gespräche Monat 1 im Kalender blocken
- Standard -
-
-
-
-
Gehaltsabrechnung vorbereiten
-
Erstes Gehalt anteilig berechnen (falls Monatsmitte) · Auszahlungstermin prüfen
- ⚡ Wichtig -
-
-
Reisekostenformulare bereitstellen
-
Spesenabrechnungsformulare und -richtlinien übergeben
- Standard -
-
-
-
- - -

Phase 4 — Der erste Arbeitstag

- -
-
⏱ Zeitpunkt
-
🧑‍💼 HR / Personal
-
💻 IT
-
👔 Vorgesetzter
-
💶 Buchhaltung / Lohn
-
-
-
-
-
Tag 1
-
Erster
Arbeitstag
-
-
-
-
Empfang & Begrüßung
-
MA am Empfang abholen · Rundgang · Schlüssel/Badge übergeben · Parkausweis
- 🔴 08:00 Uhr -
-
-
Alle Formulare unterzeichnen
-
Betriebsordnung · Datenschutz · IT-Nutzungsordnung · Schweigepflicht · Bestätigungen
- 🔴 Pflicht -
-
-
Willkommensmappe übergeben
-
Unternehmensinfos · Organigramm · Notfallnummern · Kantinen-/Pauseninfos
- Standard -
-
-
-
-
Hardware-Übergabe
-
Laptop + Zubehör übergeben · Übergabeprotokoll (Seriennummern) unterzeichnen lassen
- 🔴 Pflicht -
-
-
Erster Login begleiten
-
Passwort ändern · MFA einrichten · E-Mail testen · VPN testen · Teams einloggen
- 🔴 Pflicht -
-
-
IT-Begrüßungsmappe übergeben
-
Helpdesk-Kontakt · Passwortregeln · wichtige URLs · Verhaltensregeln bei IT-Vorfällen
- ⚡ Wichtig -
-
-
-
-
Persönliche Begrüßung
-
Nicht delegieren! Vorgesetzter begrüßt persönlich · Teamvorstellung · Buddy vorstellen
- 🔴 Pflicht -
-
-
Einarbeitungsplan besprechen
-
Ziele Probezeit · erste Aufgaben · Erwartungen klar kommunizieren · Fragen beantworten
- ⚡ Wichtig -
-
-
Gemeinsames Mittagessen
-
Mit dem Team essen gehen – sozialer Anschluss ist entscheidend!
- Standard -
-
-
-
-
Bankverbindung bestätigen
-
IBAN im System prüfen · Erste Gehaltsabrechnung korrekt hinterlegt?
- ⚡ Wichtig -
-
-
Zeiterfassung erklären
-
System vorstellen · ersten Einstempeln begleiten · Urlaubsantragsprozess erklären
- Standard -
-
-
-
- - -

Abnahme-Checkliste — Alles bereit vor Tag 1?

- -
-
- ✅ Alles auf Grün bevor der Mitarbeiter anfängt - Klicken zum Abhaken -
-
-
-
HR / Personal
-
-
0 / 6 erledigt
-
Personalakte vollständig angelegt
-
Alle Abteilungen informiert
-
Willkommens-E-Mail gesendet
-
Alle Formulare vorbereitet
-
Badge / Ausweis bestellt
-
Pflichtunterweisungen geplant
-
-
-
IT
-
-
0 / 8 erledigt
-
AD-Account angelegt & getestet
-
E-Mail-Adresse aktiv
-
Hardware aufgesetzt & getestet
-
Alle Software installiert
-
VPN & Netzwerkzugänge OK
-
Telefon / Teams eingerichtet
-
Berechtigungen vergeben
-
IT-Begrüßungsmappe bereit
-
-
-
Vorgesetzter
-
-
0 / 5 erledigt
-
Buddy / Pate benannt & gebrieft
-
30-Tage-Einarbeitungsplan fertig
-
Erste Aufgaben für Woche 1 definiert
-
Check-in-Termine im Kalender
-
Team über neuen MA informiert
-
-
-
Buchhaltung / Lohn
-
-
0 / 5 erledigt
-
Mitarbeiter in DATEV angelegt
-
IBAN & Steuerklasse hinterlegt
-
SV-Anmeldung eingereicht
-
Erstes Gehalt berechnet
-
BG-Anmeldung vollständig
-
-
-
- -
- - - - - - diff --git a/playwright-tests/check.js b/playwright-tests/check.js deleted file mode 100644 index 309747f..0000000 --- a/playwright-tests/check.js +++ /dev/null @@ -1,273 +0,0 @@ -const { chromium } = require('playwright'); -const path = require('path'); -const fs = require('fs'); - -const BASE = 'https://it-nexus.cereda-systems.de'; -const USER = 'superadmin'; -const PASS = 'Admin123!'; -const OUT = path.join(__dirname, 'screenshots'); - -if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true }); - -const issues = []; -const checks = []; - -function note(section, msg) { issues.push(`[${section}] ${msg}`); console.log(` ⚠️ ${msg}`); } -function ok(section, msg) { checks.push(`[${section}] ✓ ${msg}`); console.log(` ✅ ${msg}`); } -function info(msg) { console.log(` ℹ️ ${msg}`); } - -async function shot(page, name) { - const file = path.join(OUT, `${name}.png`); - await page.screenshot({ path: file, fullPage: true }); - console.log(` 📷 ${name}.png gespeichert`); -} - -async function waitNet(page) { - await page.waitForLoadState('networkidle').catch(() => {}); - await page.waitForTimeout(1500); -} - -(async () => { - const browser = await chromium.launch({ headless: true }); - const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); - const page = await ctx.newPage(); - - // 404-Fehler mit URL aufzeichnen - const notFound404 = []; - const consoleErrors = []; - page.on('response', r => { if (r.status() === 404) notFound404.push(r.url()); }); - page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); - - // ── 1. LOGIN ─────────────────────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🔐 LOGIN'); - await page.goto(`${BASE}/login`); - await waitNet(page); - await shot(page, '01-login'); - - const userInput = page.locator('input[type="text"], input[name="username"], input[placeholder*="user" i], input[placeholder*="name" i]').first(); - const passInput = page.locator('input[type="password"]').first(); - await userInput.fill(USER); - await passInput.fill(PASS); - await shot(page, '02-login-filled'); - await passInput.press('Enter'); - await waitNet(page); - await shot(page, '03-after-login'); - - if (page.url().includes('login')) { - note('Login', `Login fehlgeschlagen — URL: ${page.url()}`); - const errText = await page.locator('.error, [class*="error"], [class*="alert"]').first().innerText().catch(() => ''); - if (errText) note('Login', `Fehlermeldung: ${errText}`); - await browser.close(); printSummary(); return; - } - ok('Login', `Eingeloggt als ${USER} → ${page.url()}`); - - // ── 2. DASHBOARD ────────────────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('📊 DASHBOARD'); - await page.goto(`${BASE}/dashboard`); - await waitNet(page); - await shot(page, '04-dashboard'); - - const bodyText = await page.locator('body').innerText(); - if (bodyText.includes('Dashboard') || bodyText.includes('Willkommen')) ok('Dashboard', 'Seite geladen'); - else note('Dashboard', 'Dashboard-Inhalt nicht erkennbar'); - - // ── 3. MONITORING ───────────────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🖥️ MONITORING'); - await page.goto(`${BASE}/monitoring`); - await waitNet(page); - await shot(page, '05-monitoring'); - - // Geräte - const agentRows = await page.locator('tr[data-id], tbody tr, [class*="agent-row"]').count(); - info(`${agentRows} Tabellenzeilen sichtbar`); - - // Warte bis Monitoring-Tabelle geladen ist - await page.waitForTimeout(4000); - await shot(page, '05b-monitoring-loaded'); - - // IT-NB-02 per API prüfen — mit Auth-Token aus localStorage - const apiData = await page.evaluate(async () => { - try { - const token = localStorage.getItem('token') || localStorage.getItem('authToken') || - Object.keys(localStorage).map(k=>localStorage.getItem(k)).find(v=>v?.startsWith?.('eyJ')); - const headers = token ? { Authorization: `Bearer ${token}` } : {}; - const r = await fetch('/api/monitoring', { headers }); - if (!r.ok) return { error: r.status }; - const j = await r.json(); - return j.data?.find(a => a.hostname === 'IT-NB-02') || null; - } catch(e) { return { error: e.message }; } - }); - - if (apiData && !apiData.error) { - ok('Monitoring', `IT-NB-02 via API gefunden (v${apiData.agent_version})`); - info(`OS: ${apiData.os_name || '?'}`); - info(`BitLocker: ${apiData.bitlocker_status || '?'}`); - info(`Defender: enabled=${apiData.defender_enabled}, age=${apiData.defender_signatures_age}`); - info(`Serial: ${apiData.hardware_serial || '?'}`); - - if ((apiData.os_name || '').includes('11')) ok('Monitoring-API', `OS = ${apiData.os_name} ✓`); - else note('Monitoring-API', `OS = ${apiData.os_name || 'null'} — erwartet Windows 11`); - - if (apiData.bitlocker_status === 'encrypted') ok('Monitoring-API', 'BitLocker = encrypted ✓'); - else note('Monitoring-API', `BitLocker = ${apiData.bitlocker_status || 'null'}`); - - if (apiData.defender_enabled) ok('Monitoring-API', `Defender aktiv ✓ (Signaturen ${apiData.defender_signatures_age}d alt)`); - else note('Monitoring-API', 'Defender nicht aktiv oder Daten fehlen'); - - if (apiData.hardware_serial) ok('Monitoring-API', `Seriennummer = ${apiData.hardware_serial} ✓`); - else note('Monitoring-API', 'Seriennummer fehlt'); - } else { - note('Monitoring-API', `Fehler: ${apiData?.error || 'IT-NB-02 nicht gefunden'}`); - } - - // Detail-Modal über Lupen-Button öffnen - const itnb02Text = page.locator('text=IT-NB-02').first(); - if (await itnb02Text.isVisible().catch(() => false)) { - ok('Monitoring', 'IT-NB-02 in Tabelle sichtbar'); - await itnb02Text.scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - // Selektoren in Reihenfolge versuchen bis einer funktioniert - let clicked = false; - const selectors = [ - 'tr:has-text("IT-NB-02") button:first-child', - 'tr:has-text("IT-NB-02") button', - '[class*="row"]:has-text("IT-NB-02") button', - 'button:near(:text("IT-NB-02"))', - ]; - for (const sel of selectors) { - try { - const btn = page.locator(sel).first(); - if (await btn.isVisible({ timeout: 1000 }).catch(() => false)) { - await btn.click(); - clicked = true; - info(`Modal via Selektor geöffnet: ${sel}`); - break; - } - } catch { } - } - if (!clicked) { - // Letzter Fallback: direkt auf IT-NB-02 Text klicken - await itnb02Text.click(); - info('Modal via Text-Klick versucht'); - } - await page.waitForTimeout(2500); - await shot(page, '06-monitoring-detail-IT-NB-02'); - - const detail = await page.locator('body').innerText(); - - // Prüfe ob Modal offen (suche nach Sicherheits-Sektion-Keywords) - const hasModal = detail.includes('Sicherheit') || detail.includes('BitLocker') || detail.includes('Seriennummer'); - info(`Modal offen: ${hasModal} | Body-Länge: ${detail.length} Zeichen`); - - if (hasModal) { - if (detail.includes('Verschlüsselt')) ok('Monitoring-Modal', 'BitLocker = Verschlüsselt ✓'); - else note('Monitoring-Modal', 'BitLocker kein "Verschlüsselt" text'); - if (detail.includes('Defender') && detail.includes('Aktiv')) ok('Monitoring-Modal', 'Defender = Aktiv ✓'); - else note('Monitoring-Modal', 'Defender Status unklar'); - if (detail.includes('5CD44318HR')) ok('Monitoring-Modal', 'Seriennummer 5CD44318HR ✓'); - else note('Monitoring-Modal', 'Seriennummer fehlt'); - if (detail.includes('Arctic Wolf') || detail.includes('Docker') || detail.includes('IT Nexus Agent')) - ok('Monitoring-Modal', 'Software-Liste befüllt ✓'); - else note('Monitoring-Modal', 'Software fehlt'); - } else { - note('Monitoring-Modal', 'Detail-Modal hat nicht geöffnet — Sicherheits-Sektion nicht sichtbar'); - } - - await page.keyboard.press('Escape'); - await page.waitForTimeout(500); - } else { - note('Monitoring', 'IT-NB-02 nicht gefunden'); - } - - // Warnungen prüfen (BitLocker off, Defender inaktiv) - const monBody = await page.locator('body').innerText(); - if (monBody.includes('BitLocker aus')) note('Monitoring', 'Andere Geräte: BitLocker nicht aktiv — Warnung sichtbar'); - if (monBody.includes('Defender inaktiv')) note('Monitoring', 'Andere Geräte: Defender inaktiv — Warnung sichtbar'); - - // ── 4. PATCH MANAGEMENT ─────────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🔄 PATCH MANAGEMENT'); - await page.goto(`${BASE}/patch-management`); - await waitNet(page); - await shot(page, '07-patch-management'); - - const patchBody = await page.locator('body').innerText(); - - // Version - if (patchBody.includes('v2.0.0')) ok('Patch', 'v2.0.0 als aktuelle Version angezeigt ✓'); - else note('Patch', 'v2.0.0 nicht sichtbar'); - - if (patchBody.includes('1/1 aktualisiert')) ok('Patch', 'Test-Gruppe: 1/1 aktualisiert ✓'); - else note('Patch', 'Test-Gruppe nicht korrekt aktualisiert'); - - // IT-NB-02 OS im Patch-Management - const itnb02PatchRow = page.locator('tr').filter({ hasText: 'IT-NB-02' }).first(); - if (await itnb02PatchRow.isVisible().catch(() => false)) { - const rowText = await itnb02PatchRow.innerText(); - info(`IT-NB-02 Patch-Row: ${rowText.replace(/\s+/g, ' ').substring(0, 120)}`); - if (rowText.includes('Win 11') || rowText.includes('11 Pro')) ok('Patch', 'IT-NB-02 OS = Win 11 ✓'); - else if (rowText.includes('Win 10')) note('Patch', 'IT-NB-02 noch Win 10 — Check-in ausstehend'); - - if (rowText.includes('🪟')) ok('Patch', 'Win11-Badge = 🪟 Win 11 ✓'); - else if (rowText.includes('Fähig')) note('Patch', 'Win11-Badge zeigt noch "Fähig" statt "🪟 Win 11"'); - } - - // Gruppen-Tab - const gruppenTab = page.locator('text=Gruppen').first(); - if (await gruppenTab.isVisible()) { - await gruppenTab.click(); - await page.waitForTimeout(1000); - await shot(page, '08-patch-gruppen'); - } - - // ── 5. HELPDESK / TICKETS ───────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🎫 HELPDESK'); - await page.goto(`${BASE}/helpdesk`).catch(() => page.goto(`${BASE}/tickets`)); - await waitNet(page); - await shot(page, '09-helpdesk'); - const hdBody = await page.locator('body').innerText(); - if (hdBody.includes('Ticket') || hdBody.includes('Helpdesk')) ok('Helpdesk', 'Seite geladen ✓'); - else note('Helpdesk', 'Inhalt nicht erkennbar'); - - // ── 6. KI-ASSISTENT ─────────────────────────────────────────────────────── - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🤖 KI-ASSISTENT'); - await page.goto(`${BASE}/ai`).catch(() => page.goto(`${BASE}/ai-assistant`)).catch(() => {}); - await waitNet(page); - await shot(page, '10-ki'); - const kiBody = await page.locator('body').innerText(); - if (kiBody.includes('KI') || kiBody.includes('Assistent') || kiBody.includes('Claude')) ok('KI', 'KI-Assistent Seite geladen ✓'); - else note('KI', 'KI-Seite nicht gefunden oder leer'); - - // ── 7. 404 / CONSOLE ERRORS ─────────────────────────────────────────────── - if (notFound404.length > 0) { - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('🔴 404-FEHLER:'); - [...new Set(notFound404)].forEach(u => note('404', u.replace(BASE, ''))); - } - if (consoleErrors.length > 0) { - console.log('\n🔴 KONSOLEN-FEHLER:'); - [...new Set(consoleErrors)].slice(0, 5).forEach(e => info(`Console: ${e.substring(0, 100)}`)); - } - - await browser.close(); - printSummary(); -})(); - -function printSummary() { - console.log('\n' + '═'.repeat(60)); - console.log(`📊 ZUSAMMENFASSUNG: ${checks.length} OK, ${issues.length} Probleme`); - if (issues.length > 0) { - console.log('\n⚠️ ZU FIXEN:'); - issues.forEach((i, n) => console.log(` ${n+1}. ${i}`)); - } else { - console.log('✅ Alles in Ordnung!'); - } - console.log('\n📁 Screenshots: playwright-tests/screenshots/'); - console.log('═'.repeat(60) + '\n'); -} diff --git a/playwright-tests/package-lock.json b/playwright-tests/package-lock.json deleted file mode 100644 index d8bc3cb..0000000 --- a/playwright-tests/package-lock.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "playwright-tests", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "playwright-tests", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "playwright": "^1.59.1" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/playwright-tests/package.json b/playwright-tests/package.json deleted file mode 100644 index efe3f0c..0000000 --- a/playwright-tests/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "playwright-tests", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "playwright": "^1.59.1" - } -} diff --git a/playwright-tests/screenshots/01-login.png b/playwright-tests/screenshots/01-login.png deleted file mode 100644 index 207f9d8..0000000 Binary files a/playwright-tests/screenshots/01-login.png and /dev/null differ diff --git a/playwright-tests/screenshots/02-login-filled.png b/playwright-tests/screenshots/02-login-filled.png deleted file mode 100644 index 3535464..0000000 Binary files a/playwright-tests/screenshots/02-login-filled.png and /dev/null differ diff --git a/playwright-tests/screenshots/03-after-login.png b/playwright-tests/screenshots/03-after-login.png deleted file mode 100644 index d30f6e3..0000000 Binary files a/playwright-tests/screenshots/03-after-login.png and /dev/null differ diff --git a/playwright-tests/screenshots/04-dashboard.png b/playwright-tests/screenshots/04-dashboard.png deleted file mode 100644 index 4f200d3..0000000 Binary files a/playwright-tests/screenshots/04-dashboard.png and /dev/null differ diff --git a/playwright-tests/screenshots/05-monitoring.png b/playwright-tests/screenshots/05-monitoring.png deleted file mode 100644 index bf9cb89..0000000 Binary files a/playwright-tests/screenshots/05-monitoring.png and /dev/null differ diff --git a/playwright-tests/screenshots/05b-monitoring-loaded.png b/playwright-tests/screenshots/05b-monitoring-loaded.png deleted file mode 100644 index cb01c39..0000000 Binary files a/playwright-tests/screenshots/05b-monitoring-loaded.png and /dev/null differ diff --git a/playwright-tests/screenshots/06-monitoring-detail-IT-NB-02.png b/playwright-tests/screenshots/06-monitoring-detail-IT-NB-02.png deleted file mode 100644 index 1db8706..0000000 Binary files a/playwright-tests/screenshots/06-monitoring-detail-IT-NB-02.png and /dev/null differ diff --git a/playwright-tests/screenshots/07-patch-management.png b/playwright-tests/screenshots/07-patch-management.png deleted file mode 100644 index d26e750..0000000 Binary files a/playwright-tests/screenshots/07-patch-management.png and /dev/null differ diff --git a/playwright-tests/screenshots/08-patch-gruppen.png b/playwright-tests/screenshots/08-patch-gruppen.png deleted file mode 100644 index 89aa0e0..0000000 Binary files a/playwright-tests/screenshots/08-patch-gruppen.png and /dev/null differ diff --git a/playwright-tests/screenshots/09-helpdesk.png b/playwright-tests/screenshots/09-helpdesk.png deleted file mode 100644 index 3709c85..0000000 Binary files a/playwright-tests/screenshots/09-helpdesk.png and /dev/null differ diff --git a/playwright-tests/screenshots/10-ki.png b/playwright-tests/screenshots/10-ki.png deleted file mode 100644 index 79a4dbb..0000000 Binary files a/playwright-tests/screenshots/10-ki.png and /dev/null differ diff --git a/wallboardvg.aspx b/wallboardvg.aspx deleted file mode 100644 index 98b84c6..0000000 --- a/wallboardvg.aspx +++ /dev/null @@ -1 +0,0 @@ -This is a marker file generated by the precompilation tool, and should not be deleted! \ No newline at end of file