diff --git a/internal/datagen/services.go b/internal/datagen/services.go index 38834bd..e7ee398 100644 --- a/internal/datagen/services.go +++ b/internal/datagen/services.go @@ -1,5 +1,7 @@ package datagen +import "math/rand" + // ServiceStartType represents the startup type of a Windows/Linux service. type ServiceStartType string @@ -20,3 +22,110 @@ type ServiceIdentity struct { Account string // "LocalSystem", "NT AUTHORITY\NETWORK SERVICE", or a UPN SystemRef string // back-reference: hostname of owning system } + +// Service template definitions. +type serviceTemplate struct { + name string + displayName string + binaryPath string + startType ServiceStartType + account string +} + +var dcServices = []serviceTemplate{ + {"NTDS", "Active Directory Domain Services", `C:\Windows\System32\ntdsa.dll`, StartAutomatic, "LocalSystem"}, + {"DNS", "DNS Server", `C:\Windows\System32\dns.exe`, StartAutomatic, "LocalSystem"}, + {"KDC", "Kerberos Key Distribution Center", `C:\Windows\System32\lsass.exe`, StartAutomatic, "LocalSystem"}, + {"Netlogon", "Net Logon", `C:\Windows\System32\netlogon.dll`, StartAutomatic, "LocalSystem"}, + {"DFS", "DFS Replication", `C:\Windows\System32\dfsr.exe`, StartAutomatic, `NT AUTHORITY\NETWORK SERVICE`}, + {"W32Time", "Windows Time", `C:\Windows\System32\w32time.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"EventLog", "Windows Event Log", `C:\Windows\System32\wevtsvc.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"CertSvc", "Active Directory Certificate Services", `C:\Windows\System32\certsrv.exe`, StartAutomatic, "LocalSystem"}, +} + +var windowsServerServices = []serviceTemplate{ + {"W3SVC", "World Wide Web Publishing Service", `C:\Windows\System32\inetsrv\iisw3adm.dll`, StartAutomatic, "LocalSystem"}, + {"MSSQLSERVER", "SQL Server (MSSQLSERVER)", `C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Binn\sqlservr.exe`, StartAutomatic, `NT SERVICE\MSSQLSERVER`}, + {"Spooler", "Print Spooler", `C:\Windows\System32\spoolsv.exe`, StartAutomatic, "LocalSystem"}, + {"WinRM", "Windows Remote Management (WS-Management)", `C:\Windows\System32\winrm.cmd`, StartAutomatic, `NT AUTHORITY\NETWORK SERVICE`}, + {"WinDefend", "Windows Defender Antivirus Service", `C:\Program Files\Windows Defender\MsMpEng.exe`, StartAutomatic, "LocalSystem"}, + {"EventLog", "Windows Event Log", `C:\Windows\System32\wevtsvc.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"W32Time", "Windows Time", `C:\Windows\System32\w32time.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"CryptSvc", "Cryptographic Services", `C:\Windows\System32\cryptsvc.dll`, StartAutomatic, `NT AUTHORITY\NETWORK SERVICE`}, + {"wuauserv", "Windows Update", `C:\Windows\System32\wuaueng.dll`, StartAutomatic, "LocalSystem"}, + {"BITS", "Background Intelligent Transfer Service", `C:\Windows\System32\qmgr.dll`, StartManual, "LocalSystem"}, +} + +var windowsWorkstationServices = []serviceTemplate{ + {"WinDefend", "Windows Defender Antivirus Service", `C:\Program Files\Windows Defender\MsMpEng.exe`, StartAutomatic, "LocalSystem"}, + {"EventLog", "Windows Event Log", `C:\Windows\System32\wevtsvc.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"AudioSrv", "Windows Audio", `C:\Windows\System32\audiosrv.dll`, StartAutomatic, `NT AUTHORITY\LOCAL SERVICE`}, + {"Themes", "Themes", `C:\Windows\System32\themeservice.dll`, StartAutomatic, "LocalSystem"}, + {"Schedule", "Task Scheduler", `C:\Windows\System32\schedsvc.dll`, StartAutomatic, "LocalSystem"}, +} + +var linuxServerServices = []serviceTemplate{ + {"sshd", "OpenSSH server daemon", "/usr/sbin/sshd", StartAutomatic, "root"}, + {"nginx", "A high performance web server", "/usr/sbin/nginx", StartAutomatic, "www-data"}, + {"postgresql", "PostgreSQL RDBMS", "/usr/lib/postgresql/16/bin/postgres", StartAutomatic, "postgres"}, + {"docker", "Docker Application Container Engine", "/usr/bin/dockerd", StartAutomatic, "root"}, + {"cron", "Regular background program processing daemon", "/usr/sbin/cron", StartAutomatic, "root"}, + {"rsyslog", "System Logging Service", "/usr/sbin/rsyslogd", StartAutomatic, "syslog"}, + {"systemd-journald", "Journal Service", "/lib/systemd/systemd-journald", StartAutomatic, "root"}, + {"prometheus-node-exporter", "Prometheus Node Exporter", "/usr/bin/prometheus-node-exporter", StartAutomatic, "prometheus"}, +} + +var linuxWorkstationServices = []serviceTemplate{ + {"sshd", "OpenSSH server daemon", "/usr/sbin/sshd", StartAutomatic, "root"}, + {"cron", "Regular background program processing daemon", "/usr/sbin/cron", StartAutomatic, "root"}, + {"NetworkManager", "Network Manager", "/usr/sbin/NetworkManager", StartAutomatic, "root"}, +} + +// GenerateServicesForSystem returns services appropriate for the given OS and role. +func GenerateServicesForSystem(r *rand.Rand, os OSType, role SystemRole, hostname string) []*ServiceIdentity { + var templates []serviceTemplate + + switch { + case role == RoleDC: + templates = dcServices + case os == OSWindows && role == RoleServer: + templates = windowsServerServices + case os == OSWindows && role == RoleWorkstation: + templates = windowsWorkstationServices + case os == OSLinux && role == RoleServer: + templates = linuxServerServices + case os == OSLinux && role == RoleWorkstation: + templates = linuxWorkstationServices + default: + // Router or macOS — minimal services + templates = []serviceTemplate{ + {"sshd", "OpenSSH server daemon", "/usr/sbin/sshd", StartAutomatic, "root"}, + } + } + + // Pick a random subset (at least 3, up to all) + count := len(templates) + if count > 5 { + count = 3 + r.Intn(count-2) // #nosec G404 + if count > len(templates) { + count = len(templates) + } + } + + // Shuffle and take first count + indices := r.Perm(len(templates)) + services := make([]*ServiceIdentity, count) + for i := 0; i < count; i++ { + tmpl := templates[indices[i]] + services[i] = &ServiceIdentity{ + Name: tmpl.name, + DisplayName: tmpl.displayName, + BinaryPath: tmpl.binaryPath, + StartType: tmpl.startType, + Account: tmpl.account, + SystemRef: hostname, + } + } + + return services +} diff --git a/internal/datagen/services_test.go b/internal/datagen/services_test.go new file mode 100644 index 0000000..750cbe3 --- /dev/null +++ b/internal/datagen/services_test.go @@ -0,0 +1,72 @@ +package datagen + +import ( + "math/rand" + "testing" +) + +func TestGenerateServicesForSystem(t *testing.T) { + r := rand.New(rand.NewSource(42)) + + t.Run("windows server gets windows services", func(t *testing.T) { + services := GenerateServicesForSystem(r, OSWindows, RoleServer, "MARS-WEB01") + if len(services) < 3 { + t.Errorf("expected at least 3 services, got %d", len(services)) + } + for _, s := range services { + if s.SystemRef != "MARS-WEB01" { + t.Errorf("expected SystemRef 'MARS-WEB01', got %q", s.SystemRef) + } + if s.Name == "" { + t.Error("service Name should not be empty") + } + if s.DisplayName == "" { + t.Error("service DisplayName should not be empty") + } + } + }) + + t.Run("linux server gets linux services", func(t *testing.T) { + services := GenerateServicesForSystem(r, OSLinux, RoleServer, "thor-web-01") + if len(services) < 3 { + t.Errorf("expected at least 3 services, got %d", len(services)) + } + }) + + t.Run("DC gets DC-specific services", func(t *testing.T) { + services := GenerateServicesForSystem(r, OSWindows, RoleDC, "ZEUS-DC01") + hasNTDS := false + for _, s := range services { + if s.Name == "NTDS" { + hasNTDS = true + } + } + if !hasNTDS { + t.Error("DC should have NTDS service") + } + }) + + t.Run("workstation gets fewer services", func(t *testing.T) { + wsServices := GenerateServicesForSystem(r, OSWindows, RoleWorkstation, "WS01") + srvServices := GenerateServicesForSystem(r, OSWindows, RoleServer, "SRV01") + if len(wsServices) >= len(srvServices) { + t.Errorf("workstation should have fewer services (%d) than server (%d)", + len(wsServices), len(srvServices)) + } + }) + + t.Run("deterministic", func(t *testing.T) { + r1 := rand.New(rand.NewSource(99)) + r2 := rand.New(rand.NewSource(99)) + s1 := GenerateServicesForSystem(r1, OSWindows, RoleServer, "TEST") + s2 := GenerateServicesForSystem(r2, OSWindows, RoleServer, "TEST") + if len(s1) != len(s2) { + t.Fatalf("different lengths: %d vs %d", len(s1), len(s2)) + } + for i := range s1 { + if s1[i].Name != s2[i].Name { + t.Errorf("service[%d]: %q vs %q", i, s1[i].Name, s2[i].Name) + } + } + }) +}