package vulnprobe import ( "strings" "time" ) // Run executes read-only LOTL vulnerability recon and returns correlated findings. func Run(ctx HostContext) *ScanReport { findings := correlate(ctx) return finalize(findings, ctx) } func correlate(ctx HostContext) []VulnFinding { findings := make([]VulnFinding, 0, len(Catalog)) patchDays := ctx.LastPatchDays kbSet := make(map[string]bool, len(ctx.InstalledKBs)) for _, kb := range ctx.InstalledKBs { kbSet[strings.ToUpper(strings.TrimSpace(kb))] = true } for _, e := range Catalog { f := VulnFinding{ CVEID: e.ID, Severity: e.Severity, Component: e.Component, Patched: true, Detail: e.Description, } if ctx.ProbeError != "" { f.Patched = false f.Detail = "probe unavailable" findings = append(findings, f) continue } status := "clear" switch e.ID { case "CVE-2021-26855", "CVE-2020-0688": if ctx.ExchangeInstalled { status = "exposed" f.Detail = "Microsoft Exchange detected — verify Mar 2021+ CU patches" if patchDays >= 0 && patchDays > 90 { status = "likely" f.Detail += "; host patch age > 90 days" } } case "CVE-2020-1472": if ctx.IsDomainController { status = "likely" f.Detail = "Domain controller role — ensure Aug 2020 Netlogon patch applied" if patchDays >= 0 && patchDays > 60 { status = "exposed" f.Detail = "DC with patch age > 60 days — Zerologon mitigation urgency" } } case "CVE-2021-44228": if len(ctx.Log4jJars) > 0 { status = "likely" f.Detail = "log4j-core JAR(s) found: " + strings.Join(ctx.Log4jJars, "; ") } case "CVE-2019-19781": if ctx.CitrixPresent { status = "likely" f.Detail = "Citrix install paths present — verify ADC/Gateway patch level" } case "CVE-2019-11510": if ctx.PulsePresent { status = "likely" f.Detail = "Pulse/Ivanti VPN software detected" } case "CVE-2020-5902", "CVE-2022-1388": if ctx.F5Process { status = "likely" f.Detail = "F5-related process detected" } else if ctx.ListeningPorts[443] { status = "likely" f.Detail = "TCP/443 listener — verify F5/BIG-IP patch level if applicable" } case "CVE-2021-26084", "CVE-2022-26134": if ctx.ConfluenceLike { status = "likely" f.Detail = "Atlassian/Confluence-like Java process detected" } case "CVE-2021-40539": if ctx.ManageEnginePresent { status = "likely" f.Detail = "ManageEngine directory present" } case "CVE-2018-13379": if ctx.FortiClient { status = "likely" f.Detail = "Fortinet client process running" } case "CVE-2021-21972": if ctx.VMwareServerish { status = "likely" f.Detail = "VMware server-style services detected" } case "CVE-2021-34527": if ctx.SpoolerRunning && !ctx.IsDomainController { status = "likely" f.Detail = "Print Spooler running — restrict if not required" } } // KB-based patch confirmation for Windows CVEs with known mitigations. if len(e.PatchKBs) > 0 && status != "clear" { for _, kb := range e.PatchKBs { if kbSet[strings.ToUpper(kb)] { status = "clear" f.Detail = "mitigating KB " + kb + " installed" break } } } // Stale patching amplifies exposure indicators. if (status == "likely" || status == "exposed") && patchDays > 120 { f.Detail += " · OS patches older than 120 days" } f.Patched = status == "clear" f.ExploitableInFleetContext = !f.Patched && fleetExploitable(e, status, ctx) findings = append(findings, f) } // Linux package CVE hints from apt/dnf security listings. if ctx.Platform == "linux" { findings = append(findings, linuxPackageFindings(ctx)...) } return findings } func fleetExploitable(e CatalogEntry, status string, ctx HostContext) bool { if status == "clear" { return false } for _, p := range e.FleetPorts { if ctx.ListeningPorts[p] { return true } } switch e.ID { case "CVE-2020-1472": return ctx.IsDomainController case "CVE-2021-26855", "CVE-2020-0688": return ctx.ExchangeInstalled case "CVE-2021-44228": return len(ctx.Log4jJars) > 0 case "CVE-2019-19781": return ctx.CitrixPresent case "CVE-2019-11510": return ctx.PulsePresent case "CVE-2021-34527": return ctx.SpoolerRunning && ctx.ListeningPorts[445] case "CVE-2018-13379": return ctx.FortiClient || ctx.ListeningPorts[10443] } if ctx.SSHListening && (ctx.ListeningPorts[22] || ctx.ListeningPorts[2222]) { return status == "exposed" || status == "likely" } return status == "exposed" } func finalize(findings []VulnFinding, ctx HostContext) *ScanReport { r := &ScanReport{ ScannedAt: time.Now().UTC().Format(time.RFC3339), Findings: findings, } for _, f := range findings { if f.ExploitableInFleetContext { r.ExposedCount++ if f.Severity == "critical" { r.CriticalCount++ } } else if !f.Patched { r.ExposedCount++ if f.Severity == "critical" { r.CriticalCount++ } } } r.RiskScore = riskScore(r) switch { case r.ExposedCount > 0 || r.CriticalCount > 0: r.Summary = "Fleet-context vulnerability indicators detected — patch or isolate affected roles" case countUnpatched(findings) > 0: r.Summary = "Some CVE-related software stacks detected — verify versions and patches" default: r.Summary = "No high-confidence vulnerability exposure indicators on this host" } if ctx.ProbeError != "" { r.Summary = "Vulnerability probe partially unavailable" } return r } func countUnpatched(findings []VulnFinding) int { n := 0 for _, f := range findings { if !f.Patched { n++ } } return n } func riskScore(r *ScanReport) int { if r == nil { return 0 } score := r.CriticalCount*25 + r.ExposedCount*12 if score > 100 { return 100 } return score }