Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
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")
}