2026-03-19 12:08:00 +01:00
|
|
|
package main
|
|
|
|
|
|
2026-03-19 12:42:52 +01:00
|
|
|
import (
|
|
|
|
|
"context"
|
feat: Phase 8 — Network Discovery + Windows Dev-Setup-Skripte
Network Discovery:
- Go Agent: internal/scanner/scanner.go mit TCP-Sweep (Port 445/80/22/443),
ARP-Tabellen-Parser (Windows: arp -a, Linux: /proc/net/arp), Reverse-DNS,
50 gleichzeitige Goroutines mit Semaphore
- Go Agent main.go: COMMAND_TYPE_NETWORK_SCAN Case → scanner.Scan() → JSON stdout
- Backend: NetworkDevice Model (Id, AgentId, IpAddress, MacAddress, Hostname,
Vendor, IsManaged, FirstSeen, LastSeen)
- Backend: EF Migration AddNetworkDevices + Index auf IpAddress + MacAddress
- Backend: NetworkDevicesController GET /api/v1/network-devices + DELETE /{id}
- Backend: AgentGrpcService.ProcessNetworkScanResultAsync — upsert via MAC,
IsManaged=true wenn IP einem bekannten Agent entspricht
- Frontend: NetworkPage.tsx mit Scan-Panel, Device-Tabelle, Filter, Delete
- Frontend: App.tsx — 'Netzwerk' Nav-Eintrag mit Network Icon
Windows Dev-Setup:
- dev-start.ps1 — Startet Docker/Postgres, EF-Migrationen, Backend+Frontend
in separaten PowerShell-Fenstern; Voraussetzungen-Check (docker/dotnet/node/go)
- dev-stop.ps1 — Stoppt alle NexusRMM-Prozesse + PostgreSQL Container
- build-agent.ps1 — Baut nexus-agent.exe (Windows) + optional nexus-agent-linux
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:53:35 +01:00
|
|
|
"encoding/json"
|
2026-03-19 12:42:52 +01:00
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
2026-03-20 10:24:00 +01:00
|
|
|
"path/filepath"
|
2026-03-19 12:42:52 +01:00
|
|
|
"runtime"
|
2026-03-19 15:29:11 +01:00
|
|
|
"strings"
|
2026-03-19 12:42:52 +01:00
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"nexusrmm.local/agent/internal/collector"
|
|
|
|
|
"nexusrmm.local/agent/internal/config"
|
|
|
|
|
"nexusrmm.local/agent/internal/connection"
|
feat: implement Phase 6 — Software Deployment
Backend:
- SoftwarePackage model (Name, Version, OsType, PackageManager, PackageName, InstallerUrl, Checksum, SilentArgs)
- RmmDbContext: SoftwarePackages DbSet + unique index on (Name, Version, OsType)
- SoftwarePackagesController: full CRUD with OsType filter
- DeployController: POST /api/v1/deploy creates InstallSoftware/UninstallSoftware TaskItem
- EF Migration: AddSoftwarePackages (20260319130448)
Go Agent:
- internal/deployer/deployer.go: Install() and Uninstall() with:
- Chocolatey (Windows), apt/dnf (Linux), auto-detect
- Direct installer fallback: HTTP download + SHA256 verify + silent install
- Supports .msi, .exe (Windows) and .deb, .rpm (Linux)
- main.go: COMMAND_TYPE_INSTALL_SOFTWARE and COMMAND_TYPE_UNINSTALL_SOFTWARE routed to deployer
Frontend:
- SoftwarePage: Katalog tab (CRUD, OS filter, smart package manager select) + Deploy tab
- api/types.ts: SoftwarePackage, PackageManager, DeployRequest/Response types
- api/client.ts: softwarePackagesApi and deployApi
- App.tsx: Software nav item with Package icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:06:40 +01:00
|
|
|
"nexusrmm.local/agent/internal/deployer"
|
2026-03-19 12:42:52 +01:00
|
|
|
"nexusrmm.local/agent/internal/executor"
|
2026-03-19 14:39:49 +01:00
|
|
|
"nexusrmm.local/agent/internal/meshagent"
|
feat: Phase 8 — Network Discovery + Windows Dev-Setup-Skripte
Network Discovery:
- Go Agent: internal/scanner/scanner.go mit TCP-Sweep (Port 445/80/22/443),
ARP-Tabellen-Parser (Windows: arp -a, Linux: /proc/net/arp), Reverse-DNS,
50 gleichzeitige Goroutines mit Semaphore
- Go Agent main.go: COMMAND_TYPE_NETWORK_SCAN Case → scanner.Scan() → JSON stdout
- Backend: NetworkDevice Model (Id, AgentId, IpAddress, MacAddress, Hostname,
Vendor, IsManaged, FirstSeen, LastSeen)
- Backend: EF Migration AddNetworkDevices + Index auf IpAddress + MacAddress
- Backend: NetworkDevicesController GET /api/v1/network-devices + DELETE /{id}
- Backend: AgentGrpcService.ProcessNetworkScanResultAsync — upsert via MAC,
IsManaged=true wenn IP einem bekannten Agent entspricht
- Frontend: NetworkPage.tsx mit Scan-Panel, Device-Tabelle, Filter, Delete
- Frontend: App.tsx — 'Netzwerk' Nav-Eintrag mit Network Icon
Windows Dev-Setup:
- dev-start.ps1 — Startet Docker/Postgres, EF-Migrationen, Backend+Frontend
in separaten PowerShell-Fenstern; Voraussetzungen-Check (docker/dotnet/node/go)
- dev-stop.ps1 — Stoppt alle NexusRMM-Prozesse + PostgreSQL Container
- build-agent.ps1 — Baut nexus-agent.exe (Windows) + optional nexus-agent-linux
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:53:35 +01:00
|
|
|
"nexusrmm.local/agent/internal/scanner"
|
feat: Phase 9 — Offline Detection, API Key Auth, Agent Self-Update
Offline Detection (9.1):
- AgentOfflineDetectorService: BackgroundService, prüft alle 60s
ob Agents seit >5 min kein Heartbeat hatten → Status=Offline
- IServiceScopeFactory für korrektes Scoped-DI im Singleton
- SignalR-Push AgentStatusChanged bei jeder Offline-Markierung
API Key Auth (9.2):
- ApiKeyMiddleware: prüft X-Api-Key Header gegen Security:ApiKey Config
- Deaktiviert wenn ApiKey leer (Dev-Modus), Swagger/hubs bypassed
- Frontend: getApiKey() aus localStorage, automatisch in allen Requests
- Settings-Modal in Sidebar: API-Key eingeben + maskiert anzeigen
Agent Self-Update (9.3):
- internal/updater/updater.go: CheckForUpdate() + Update()
Download, SHA256-Verify, Windows Batch-Neustart / Linux Shell-Neustart
- AgentReleasesController: GET /api/v1/agent/releases/latest,
GET /api/v1/agent/releases/download/{platform}
- AgentReleaseOptions: LatestVersion, ReleasePath, Checksum in appsettings
- executeCommand() erhält cfg *Config statt agentID string
(für ServerAddress-Ableitung im UpdateAgent-Case)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:41:24 +01:00
|
|
|
"nexusrmm.local/agent/internal/updater"
|
2026-03-20 10:24:00 +01:00
|
|
|
"nexusrmm.local/agent/internal/winsvc"
|
2026-03-19 12:42:52 +01:00
|
|
|
pb "nexusrmm.local/agent/pkg/proto"
|
|
|
|
|
)
|
2026-03-19 12:08:00 +01:00
|
|
|
|
|
|
|
|
var version = "dev"
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-03-20 10:24:00 +01:00
|
|
|
// Absoluten Pfad zur EXE ermitteln — kritisch im Service-Modus,
|
|
|
|
|
// da der Working Directory dann C:\Windows\System32 ist.
|
|
|
|
|
exePath, err := os.Executable()
|
2026-03-19 12:42:52 +01:00
|
|
|
if err != nil {
|
2026-03-20 10:24:00 +01:00
|
|
|
log.Fatalf("Konnte EXE-Pfad nicht ermitteln: %v", err)
|
2026-03-19 12:42:52 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
exeDir := filepath.Dir(exePath)
|
|
|
|
|
configPath := filepath.Join(exeDir, "nexus-agent.yaml")
|
2026-03-19 12:42:52 +01:00
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
// Kommandozeilen-Argument verarbeiten (install / uninstall / start / stop)
|
|
|
|
|
if len(os.Args) > 1 {
|
|
|
|
|
switch os.Args[1] {
|
|
|
|
|
case "install":
|
|
|
|
|
if err := winsvc.Install(exePath); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println("Service erfolgreich installiert.")
|
|
|
|
|
return
|
|
|
|
|
case "uninstall":
|
|
|
|
|
if err := winsvc.Uninstall(); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
2026-03-19 15:29:11 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
fmt.Println("Service erfolgreich deinstalliert.")
|
|
|
|
|
return
|
|
|
|
|
case "start":
|
|
|
|
|
if err := winsvc.Start(); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
2026-03-19 15:29:11 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
fmt.Println("Service gestartet.")
|
|
|
|
|
return
|
|
|
|
|
case "stop":
|
|
|
|
|
if err := winsvc.Stop(); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
2026-03-19 15:29:11 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
fmt.Println("Service gestoppt.")
|
|
|
|
|
return
|
2026-03-19 12:42:52 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
}
|
2026-03-19 12:42:52 +01:00
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
// Prüfen ob wir als Windows Service laufen
|
|
|
|
|
isSvc, err := winsvc.IsWindowsService()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Service-Erkennung fehlgeschlagen: %v", err)
|
2026-03-19 12:42:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
if isSvc {
|
|
|
|
|
// Als Windows Service starten — blockiert bis Service gestoppt wird
|
|
|
|
|
if err := winsvc.Run(makeRunFn(configPath)); err != nil {
|
|
|
|
|
log.Fatalf("Service-Fehler: %v", err)
|
2026-03-19 14:39:49 +01:00
|
|
|
}
|
2026-03-20 10:24:00 +01:00
|
|
|
return
|
2026-03-19 14:39:49 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
// Konsolen-Modus: direkt ausführen mit Ctrl+C-Signal
|
|
|
|
|
log.Printf("NexusRMM Agent %s starting on %s/%s", version, runtime.GOOS, runtime.GOARCH)
|
|
|
|
|
|
|
|
|
|
stop := make(chan struct{})
|
2026-03-19 12:42:52 +01:00
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
2026-03-20 10:24:00 +01:00
|
|
|
go func() {
|
|
|
|
|
<-sigCh
|
|
|
|
|
log.Println("Shutting down...")
|
|
|
|
|
close(stop)
|
|
|
|
|
}()
|
2026-03-19 12:42:52 +01:00
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
makeRunFn(configPath)(stop)
|
|
|
|
|
}
|
2026-03-19 12:42:52 +01:00
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
// makeRunFn gibt die Haupt-Agent-Loop als Funktion zurück.
|
|
|
|
|
// stop wird geschlossen wenn der Agent beendet werden soll (Service-Stop oder Ctrl+C).
|
|
|
|
|
func makeRunFn(configPath string) func(stop <-chan struct{}) {
|
|
|
|
|
return func(stop <-chan struct{}) {
|
|
|
|
|
log.Printf("NexusRMM Agent %s starting on %s/%s", version, runtime.GOOS, runtime.GOARCH)
|
2026-03-19 12:42:52 +01:00
|
|
|
|
2026-03-20 10:24:00 +01:00
|
|
|
cfg, err := config.Load(configPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Config load error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := connection.ConnectWithRetry(cfg.ServerAddress, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to connect: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
if cfg.AgentID == "" {
|
|
|
|
|
hostname, _ := os.Hostname()
|
|
|
|
|
metrics, _ := collector.Collect()
|
|
|
|
|
mac, ip := "", ""
|
|
|
|
|
for _, n := range metrics.Networks {
|
|
|
|
|
if n.MAC == "" || n.IPAddress == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if mac == "" {
|
|
|
|
|
mac, ip = n.MAC, n.IPAddress
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(n.IPAddress, ":") {
|
|
|
|
|
mac, ip = n.MAC, n.IPAddress
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.Client.Enroll(context.Background(), &pb.EnrollRequest{
|
|
|
|
|
Hostname: hostname,
|
|
|
|
|
OsType: runtime.GOOS,
|
|
|
|
|
OsVersion: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
|
|
|
|
MacAddress: mac,
|
|
|
|
|
IpAddress: ip,
|
|
|
|
|
AgentVersion: version,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Enrollment failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
cfg.AgentID = resp.AgentId
|
|
|
|
|
cfg.HeartbeatInterval = int(resp.HeartbeatInterval)
|
|
|
|
|
if err := cfg.Save(configPath); err != nil {
|
|
|
|
|
log.Printf("Warning: could not save config: %v", err)
|
|
|
|
|
}
|
|
|
|
|
log.Printf("Enrolled with ID: %s", cfg.AgentID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.MeshEnabled && cfg.MeshCentralUrl != "" {
|
|
|
|
|
log.Printf("Installiere MeshAgent von %s...", cfg.MeshCentralUrl)
|
|
|
|
|
if err := meshagent.Install(context.Background(), cfg.MeshCentralUrl); err != nil {
|
|
|
|
|
log.Printf("MeshAgent-Installation fehlgeschlagen (nicht kritisch): %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
ticker := time.NewTicker(time.Duration(cfg.HeartbeatInterval) * time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
log.Printf("Agent running. Heartbeat every %ds", cfg.HeartbeatInterval)
|
|
|
|
|
doHeartbeat(ctx, client, cfg)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
doHeartbeat(ctx, client, cfg)
|
|
|
|
|
case <-stop:
|
|
|
|
|
log.Println("Agent wird beendet...")
|
|
|
|
|
return
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-19 12:42:52 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func doHeartbeat(ctx context.Context, client *connection.GrpcClient, cfg *config.Config) {
|
|
|
|
|
metrics, err := collector.Collect()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Metric collection error: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := &pb.HeartbeatRequest{
|
|
|
|
|
AgentId: cfg.AgentID,
|
|
|
|
|
Metrics: &pb.SystemMetrics{
|
|
|
|
|
CpuUsagePercent: metrics.CPUPercent,
|
|
|
|
|
MemoryUsagePercent: metrics.MemoryPercent,
|
|
|
|
|
MemoryTotalBytes: int64(metrics.MemoryTotal),
|
|
|
|
|
MemoryAvailableBytes: int64(metrics.MemoryAvailable),
|
|
|
|
|
UptimeSeconds: metrics.UptimeSeconds,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, d := range metrics.Disks {
|
|
|
|
|
req.Metrics.Disks = append(req.Metrics.Disks, &pb.DiskInfo{
|
|
|
|
|
MountPoint: d.MountPoint,
|
|
|
|
|
TotalBytes: int64(d.Total),
|
|
|
|
|
FreeBytes: int64(d.Free),
|
|
|
|
|
Filesystem: d.Filesystem,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.Client.Heartbeat(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Heartbeat error: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, cmd := range resp.PendingCommands {
|
|
|
|
|
log.Printf("Executing command %s (type: %v)", cmd.CommandId, cmd.Type)
|
feat: Phase 9 — Offline Detection, API Key Auth, Agent Self-Update
Offline Detection (9.1):
- AgentOfflineDetectorService: BackgroundService, prüft alle 60s
ob Agents seit >5 min kein Heartbeat hatten → Status=Offline
- IServiceScopeFactory für korrektes Scoped-DI im Singleton
- SignalR-Push AgentStatusChanged bei jeder Offline-Markierung
API Key Auth (9.2):
- ApiKeyMiddleware: prüft X-Api-Key Header gegen Security:ApiKey Config
- Deaktiviert wenn ApiKey leer (Dev-Modus), Swagger/hubs bypassed
- Frontend: getApiKey() aus localStorage, automatisch in allen Requests
- Settings-Modal in Sidebar: API-Key eingeben + maskiert anzeigen
Agent Self-Update (9.3):
- internal/updater/updater.go: CheckForUpdate() + Update()
Download, SHA256-Verify, Windows Batch-Neustart / Linux Shell-Neustart
- AgentReleasesController: GET /api/v1/agent/releases/latest,
GET /api/v1/agent/releases/download/{platform}
- AgentReleaseOptions: LatestVersion, ReleasePath, Checksum in appsettings
- executeCommand() erhält cfg *Config statt agentID string
(für ServerAddress-Ableitung im UpdateAgent-Case)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:41:24 +01:00
|
|
|
go executeCommand(ctx, client, cfg, cmd)
|
2026-03-19 12:42:52 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Phase 9 — Offline Detection, API Key Auth, Agent Self-Update
Offline Detection (9.1):
- AgentOfflineDetectorService: BackgroundService, prüft alle 60s
ob Agents seit >5 min kein Heartbeat hatten → Status=Offline
- IServiceScopeFactory für korrektes Scoped-DI im Singleton
- SignalR-Push AgentStatusChanged bei jeder Offline-Markierung
API Key Auth (9.2):
- ApiKeyMiddleware: prüft X-Api-Key Header gegen Security:ApiKey Config
- Deaktiviert wenn ApiKey leer (Dev-Modus), Swagger/hubs bypassed
- Frontend: getApiKey() aus localStorage, automatisch in allen Requests
- Settings-Modal in Sidebar: API-Key eingeben + maskiert anzeigen
Agent Self-Update (9.3):
- internal/updater/updater.go: CheckForUpdate() + Update()
Download, SHA256-Verify, Windows Batch-Neustart / Linux Shell-Neustart
- AgentReleasesController: GET /api/v1/agent/releases/latest,
GET /api/v1/agent/releases/download/{platform}
- AgentReleaseOptions: LatestVersion, ReleasePath, Checksum in appsettings
- executeCommand() erhält cfg *Config statt agentID string
(für ServerAddress-Ableitung im UpdateAgent-Case)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:41:24 +01:00
|
|
|
func executeCommand(ctx context.Context, client *connection.GrpcClient, cfg *config.Config, cmd *pb.AgentCommand) {
|
|
|
|
|
agentID := cfg.AgentID
|
2026-03-19 12:42:52 +01:00
|
|
|
var result *executor.Result
|
|
|
|
|
switch cmd.Type {
|
|
|
|
|
case pb.CommandType_COMMAND_TYPE_SHELL:
|
|
|
|
|
result = executor.Execute(ctx, cmd.Payload, 300)
|
feat: implement Phase 6 — Software Deployment
Backend:
- SoftwarePackage model (Name, Version, OsType, PackageManager, PackageName, InstallerUrl, Checksum, SilentArgs)
- RmmDbContext: SoftwarePackages DbSet + unique index on (Name, Version, OsType)
- SoftwarePackagesController: full CRUD with OsType filter
- DeployController: POST /api/v1/deploy creates InstallSoftware/UninstallSoftware TaskItem
- EF Migration: AddSoftwarePackages (20260319130448)
Go Agent:
- internal/deployer/deployer.go: Install() and Uninstall() with:
- Chocolatey (Windows), apt/dnf (Linux), auto-detect
- Direct installer fallback: HTTP download + SHA256 verify + silent install
- Supports .msi, .exe (Windows) and .deb, .rpm (Linux)
- main.go: COMMAND_TYPE_INSTALL_SOFTWARE and COMMAND_TYPE_UNINSTALL_SOFTWARE routed to deployer
Frontend:
- SoftwarePage: Katalog tab (CRUD, OS filter, smart package manager select) + Deploy tab
- api/types.ts: SoftwarePackage, PackageManager, DeployRequest/Response types
- api/client.ts: softwarePackagesApi and deployApi
- App.tsx: Software nav item with Package icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:06:40 +01:00
|
|
|
case pb.CommandType_COMMAND_TYPE_INSTALL_SOFTWARE:
|
|
|
|
|
result = deployer.Install(ctx, cmd.Payload)
|
|
|
|
|
case pb.CommandType_COMMAND_TYPE_UNINSTALL_SOFTWARE:
|
|
|
|
|
result = deployer.Uninstall(ctx, cmd.Payload)
|
feat: Phase 8 — Network Discovery + Windows Dev-Setup-Skripte
Network Discovery:
- Go Agent: internal/scanner/scanner.go mit TCP-Sweep (Port 445/80/22/443),
ARP-Tabellen-Parser (Windows: arp -a, Linux: /proc/net/arp), Reverse-DNS,
50 gleichzeitige Goroutines mit Semaphore
- Go Agent main.go: COMMAND_TYPE_NETWORK_SCAN Case → scanner.Scan() → JSON stdout
- Backend: NetworkDevice Model (Id, AgentId, IpAddress, MacAddress, Hostname,
Vendor, IsManaged, FirstSeen, LastSeen)
- Backend: EF Migration AddNetworkDevices + Index auf IpAddress + MacAddress
- Backend: NetworkDevicesController GET /api/v1/network-devices + DELETE /{id}
- Backend: AgentGrpcService.ProcessNetworkScanResultAsync — upsert via MAC,
IsManaged=true wenn IP einem bekannten Agent entspricht
- Frontend: NetworkPage.tsx mit Scan-Panel, Device-Tabelle, Filter, Delete
- Frontend: App.tsx — 'Netzwerk' Nav-Eintrag mit Network Icon
Windows Dev-Setup:
- dev-start.ps1 — Startet Docker/Postgres, EF-Migrationen, Backend+Frontend
in separaten PowerShell-Fenstern; Voraussetzungen-Check (docker/dotnet/node/go)
- dev-stop.ps1 — Stoppt alle NexusRMM-Prozesse + PostgreSQL Container
- build-agent.ps1 — Baut nexus-agent.exe (Windows) + optional nexus-agent-linux
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 14:53:35 +01:00
|
|
|
case pb.CommandType_COMMAND_TYPE_NETWORK_SCAN:
|
|
|
|
|
var params struct {
|
|
|
|
|
Subnet string `json:"subnet"`
|
|
|
|
|
}
|
|
|
|
|
_ = json.Unmarshal([]byte(cmd.Payload), ¶ms)
|
|
|
|
|
devices, err := scanner.Scan(ctx, params.Subnet)
|
|
|
|
|
if err != nil {
|
|
|
|
|
result = &executor.Result{ExitCode: 1, Stderr: err.Error()}
|
|
|
|
|
} else {
|
|
|
|
|
result = &executor.Result{
|
|
|
|
|
ExitCode: 0,
|
|
|
|
|
Stdout: scanner.ToJSON(devices),
|
|
|
|
|
Success: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: Phase 9 — Offline Detection, API Key Auth, Agent Self-Update
Offline Detection (9.1):
- AgentOfflineDetectorService: BackgroundService, prüft alle 60s
ob Agents seit >5 min kein Heartbeat hatten → Status=Offline
- IServiceScopeFactory für korrektes Scoped-DI im Singleton
- SignalR-Push AgentStatusChanged bei jeder Offline-Markierung
API Key Auth (9.2):
- ApiKeyMiddleware: prüft X-Api-Key Header gegen Security:ApiKey Config
- Deaktiviert wenn ApiKey leer (Dev-Modus), Swagger/hubs bypassed
- Frontend: getApiKey() aus localStorage, automatisch in allen Requests
- Settings-Modal in Sidebar: API-Key eingeben + maskiert anzeigen
Agent Self-Update (9.3):
- internal/updater/updater.go: CheckForUpdate() + Update()
Download, SHA256-Verify, Windows Batch-Neustart / Linux Shell-Neustart
- AgentReleasesController: GET /api/v1/agent/releases/latest,
GET /api/v1/agent/releases/download/{platform}
- AgentReleaseOptions: LatestVersion, ReleasePath, Checksum in appsettings
- executeCommand() erhält cfg *Config statt agentID string
(für ServerAddress-Ableitung im UpdateAgent-Case)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:41:24 +01:00
|
|
|
case pb.CommandType_COMMAND_TYPE_UPDATE_AGENT:
|
|
|
|
|
var params struct {
|
|
|
|
|
ServerAddress string `json:"serverAddress"`
|
|
|
|
|
}
|
|
|
|
|
_ = json.Unmarshal([]byte(cmd.Payload), ¶ms)
|
|
|
|
|
if params.ServerAddress == "" {
|
|
|
|
|
params.ServerAddress = strings.Replace(cfg.ServerAddress, "5001", "5000", 1)
|
|
|
|
|
}
|
|
|
|
|
info, err := updater.CheckForUpdate(ctx, params.ServerAddress, version)
|
|
|
|
|
if err != nil {
|
|
|
|
|
result = &executor.Result{ExitCode: 1, Stderr: err.Error()}
|
|
|
|
|
} else if info == nil {
|
|
|
|
|
result = &executor.Result{ExitCode: 0, Stdout: "Bereits aktuell: " + version, Success: true}
|
|
|
|
|
} else {
|
|
|
|
|
if updateErr := updater.Update(ctx, info); updateErr != nil {
|
|
|
|
|
result = &executor.Result{ExitCode: 1, Stderr: updateErr.Error()}
|
|
|
|
|
} else {
|
|
|
|
|
result = &executor.Result{ExitCode: 0, Stdout: "Update auf " + info.Version + " gestartet", Success: true}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-19 12:42:52 +01:00
|
|
|
default:
|
|
|
|
|
result = &executor.Result{ExitCode: -1, Stderr: fmt.Sprintf("unknown command type: %v", cmd.Type)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := func() error {
|
|
|
|
|
_, err := client.Client.ReportCommandResult(ctx, &pb.CommandResult{
|
|
|
|
|
AgentId: agentID,
|
|
|
|
|
CommandId: cmd.CommandId,
|
|
|
|
|
ExitCode: int32(result.ExitCode),
|
|
|
|
|
Stdout: result.Stdout,
|
|
|
|
|
Stderr: result.Stderr,
|
|
|
|
|
Success: result.Success,
|
|
|
|
|
})
|
|
|
|
|
return err
|
|
|
|
|
}(); err != nil {
|
|
|
|
|
log.Printf("Failed to report result for %s: %v", cmd.CommandId, err)
|
|
|
|
|
}
|
2026-03-19 12:08:00 +01:00
|
|
|
}
|