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,129 @@
// 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
}

View File

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