package strategy import ( "net" "strings" ) // HostFingerprint captures host signals that drive fleet-wide tier scoring. type HostFingerprint struct { GOOS string `json:"goos"` Docker bool `json:"docker"` WSL bool `json:"wsl"` GPU bool `json:"gpu"` AVBlocks bool `json:"av_blocks"` DomainJoined bool `json:"domain_joined"` Subnet string `json:"subnet"` } // Key returns a stable bucket id for fleet outcome aggregation. func (f HostFingerprint) Key() string { goos := strings.ToLower(strings.TrimSpace(f.GOOS)) if goos == "" { goos = "unknown" } subnet := strings.TrimSpace(f.Subnet) if subnet == "" { subnet = "unknown" } return strings.Join([]string{ goos, boolToken(f.Docker), boolToken(f.WSL), boolToken(f.GPU), boolToken(f.AVBlocks), boolToken(f.DomainJoined), subnet, }, "|") } func boolToken(v bool) string { if v { return "1" } return "0" } // FingerprintFromAuth builds an initial fingerprint from agent auth + connection IP. func FingerprintFromAuth(platform, ip string, domainJoined bool) HostFingerprint { return HostFingerprint{ GOOS: normalizeGOOS(platform), DomainJoined: domainJoined, Subnet: subnetFromIP(ip), } } // FingerprintFromStats merges live telemetry probes into a fingerprint. func FingerprintFromStats(base HostFingerprint, probes map[string]bool, domainJoined *bool) HostFingerprint { out := base if probes != nil { if v, ok := probes["docker"]; ok { out.Docker = v } if v, ok := probes["wsl"]; ok { out.WSL = v } if v, ok := probes["gpu"]; ok { out.GPU = v } if v, ok := probes["av_blocks_exe"]; ok { out.AVBlocks = v } } if domainJoined != nil { out.DomainJoined = *domainJoined } return out } func normalizeGOOS(platform string) string { p := strings.ToLower(strings.TrimSpace(platform)) switch { case strings.Contains(p, "win"): return "windows" case strings.Contains(p, "linux"): return "linux" case strings.Contains(p, "darwin"), strings.Contains(p, "mac"): return "darwin" default: return p } } func subnetFromIP(ip string) string { ip = strings.TrimSpace(ip) if ip == "" { return "" } host := ip if h, _, err := net.SplitHostPort(ip); err == nil { host = h } parsed := net.ParseIP(host) if parsed == nil { return "" } if v4 := parsed.To4(); v4 != nil { parts := strings.Split(host, ".") if len(parts) >= 3 { return strings.Join(parts[:3], ".") } } if strings.Contains(host, ":") { parts := strings.Split(host, ":") if len(parts) >= 3 { return strings.Join(parts[:3], ":") } } return host }