From 0e751c52a2af8b02c3df4c152efb1612aa9dcb18 Mon Sep 17 00:00:00 2001 From: Dylan Myers Date: Mon, 6 Apr 2026 12:47:46 -0400 Subject: [PATCH] feat(datagen): add ApplicationIdentity with per-role app allocation --- internal/datagen/applications.go | 99 +++++++++++++++++++++++++++ internal/datagen/applications_test.go | 67 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 internal/datagen/applications_test.go diff --git a/internal/datagen/applications.go b/internal/datagen/applications.go index 41e7a23..f35666b 100644 --- a/internal/datagen/applications.go +++ b/internal/datagen/applications.go @@ -1,5 +1,7 @@ package datagen +import "math/rand" + // ApplicationIdentity represents installed software on a system. type ApplicationIdentity struct { Name string // "Microsoft SQL Server 2019" @@ -9,3 +11,100 @@ type ApplicationIdentity struct { InstallDate string // "2024-06-15" SystemRef string // back-reference: hostname of owning system } + +// appTemplate defines an application template. +type appTemplate struct { + name string + version string + vendor string + installPath string +} + +var windowsServerApps = []appTemplate{ + {"Internet Information Services", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32\inetsrv`}, + {"Microsoft SQL Server 2019", "15.0.4322.2", "Microsoft Corporation", `C:\Program Files\Microsoft SQL Server`}, + {".NET Runtime 8.0", "8.0.1", "Microsoft Corporation", `C:\Program Files\dotnet`}, + {"Microsoft Exchange Server 2019", "15.2.1118.40", "Microsoft Corporation", `C:\Program Files\Microsoft\Exchange Server`}, + {"Microsoft SharePoint Server 2019", "16.0.10396.20000", "Microsoft Corporation", `C:\Program Files\Microsoft Office Servers`}, + {"Windows Server Backup", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32`}, + {"Microsoft Visual C++ 2019 Redistributable", "14.29.30133", "Microsoft Corporation", `C:\Program Files\Microsoft Visual Studio`}, +} + +var linuxServerApps = []appTemplate{ + {"nginx", "1.24.0", "Nginx Inc.", "/usr/sbin/nginx"}, + {"Apache HTTP Server", "2.4.58", "Apache Software Foundation", "/usr/sbin/apache2"}, + {"PostgreSQL", "16.1", "PostgreSQL Global Development Group", "/usr/lib/postgresql/16"}, + {"MySQL", "8.0.35", "Oracle Corporation", "/usr/sbin/mysqld"}, + {"Docker Engine", "24.0.7", "Docker Inc.", "/usr/bin/docker"}, + {"OpenJDK Runtime Environment", "17.0.9", "Eclipse Adoptium", "/usr/lib/jvm/java-17-openjdk"}, + {"Node.js", "20.10.0", "OpenJS Foundation", "/usr/bin/node"}, + {"Redis", "7.2.3", "Redis Ltd.", "/usr/bin/redis-server"}, +} + +var workstationApps = []appTemplate{ + {"Microsoft Office 365 ProPlus", "16.0.17126.20132", "Microsoft Corporation", `C:\Program Files\Microsoft Office`}, + {"Google Chrome", "120.0.6099.130", "Google LLC", `C:\Program Files\Google\Chrome`}, + {"Mozilla Firefox", "121.0", "Mozilla Foundation", `C:\Program Files\Mozilla Firefox`}, + {"Visual Studio Code", "1.85.1", "Microsoft Corporation", `C:\Users\AppData\Local\Programs\Microsoft VS Code`}, + {"Slack", "4.35.126", "Slack Technologies", `C:\Users\AppData\Local\slack`}, + {"Microsoft Teams", "1.6.00.36062", "Microsoft Corporation", `C:\Users\AppData\Local\Microsoft\Teams`}, + {"Zoom Workplace", "5.17.0", "Zoom Video Communications", `C:\Program Files\Zoom`}, + {"Adobe Acrobat Reader", "23.008.20470", "Adobe Inc.", `C:\Program Files\Adobe\Acrobat Reader DC`}, +} + +var dcApps = []appTemplate{ + {"Active Directory Domain Services", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32`}, + {"Active Directory Certificate Services", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32\certsrv`}, + {"DNS Server", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32\dns.exe`}, + {"DHCP Server", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32`}, + {"Remote Server Administration Tools", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32`}, + {"Group Policy Management Console", "10.0.20348", "Microsoft Corporation", `C:\Windows\System32`}, +} + +// installDates provides deterministic install date strings. +var installDates = NewPool( + "2024-01-15", "2024-02-20", "2024-03-10", "2024-04-05", + "2024-05-18", "2024-06-22", "2024-07-30", "2024-08-14", + "2024-09-01", "2024-10-12", "2024-11-25", "2024-12-03", +) + +// GenerateApplicationsForSystem returns applications appropriate for the given OS and role. +func GenerateApplicationsForSystem(r *rand.Rand, os OSType, role SystemRole, hostname string) []*ApplicationIdentity { + var templates []appTemplate + + switch { + case role == RoleDC: + templates = dcApps + case os == OSWindows && role == RoleServer: + templates = windowsServerApps + case os == OSLinux && (role == RoleServer || role == RoleRouter): + templates = linuxServerApps + default: + templates = workstationApps + } + + // Pick a random subset (at least 2, up to all) + count := len(templates) + if count > 3 { + count = 2 + r.Intn(count-1) // #nosec G404 + if count > len(templates) { + count = len(templates) + } + } + + indices := r.Perm(len(templates)) + apps := make([]*ApplicationIdentity, count) + for i := 0; i < count; i++ { + tmpl := templates[indices[i]] + apps[i] = &ApplicationIdentity{ + Name: tmpl.name, + Version: tmpl.version, + Vendor: tmpl.vendor, + InstallPath: tmpl.installPath, + InstallDate: installDates.Random(r), + SystemRef: hostname, + } + } + + return apps +} diff --git a/internal/datagen/applications_test.go b/internal/datagen/applications_test.go new file mode 100644 index 0000000..16d1744 --- /dev/null +++ b/internal/datagen/applications_test.go @@ -0,0 +1,67 @@ +package datagen + +import ( + "math/rand" + "testing" +) + +func TestGenerateApplicationsForSystem(t *testing.T) { + r := rand.New(rand.NewSource(42)) + + t.Run("windows server apps", func(t *testing.T) { + apps := GenerateApplicationsForSystem(r, OSWindows, RoleServer, "SRV01") + if len(apps) < 2 { + t.Errorf("expected at least 2 apps, got %d", len(apps)) + } + for _, a := range apps { + if a.SystemRef != "SRV01" { + t.Errorf("expected SystemRef 'SRV01', got %q", a.SystemRef) + } + if a.Name == "" { + t.Error("app Name should not be empty") + } + if a.Version == "" { + t.Error("app Version should not be empty") + } + if a.Vendor == "" { + t.Error("app Vendor should not be empty") + } + } + }) + + t.Run("linux server apps", func(t *testing.T) { + apps := GenerateApplicationsForSystem(r, OSLinux, RoleServer, "srv01") + if len(apps) < 2 { + t.Errorf("expected at least 2 apps, got %d", len(apps)) + } + }) + + t.Run("DC apps", func(t *testing.T) { + apps := GenerateApplicationsForSystem(r, OSWindows, RoleDC, "DC01") + if len(apps) < 2 { + t.Errorf("expected at least 2 apps, got %d", len(apps)) + } + }) + + t.Run("workstation apps", func(t *testing.T) { + apps := GenerateApplicationsForSystem(r, OSWindows, RoleWorkstation, "WS01") + if len(apps) < 2 { + t.Errorf("expected at least 2 apps, got %d", len(apps)) + } + }) + + t.Run("deterministic", func(t *testing.T) { + r1 := rand.New(rand.NewSource(99)) + r2 := rand.New(rand.NewSource(99)) + a1 := GenerateApplicationsForSystem(r1, OSWindows, RoleServer, "T") + a2 := GenerateApplicationsForSystem(r2, OSWindows, RoleServer, "T") + if len(a1) != len(a2) { + t.Fatalf("different lengths: %d vs %d", len(a1), len(a2)) + } + for i := range a1 { + if a1[i].Name != a2[i].Name { + t.Errorf("app[%d]: %q vs %q", i, a1[i].Name, a2[i].Name) + } + } + }) +}