220 lines
5.7 KiB
Go
220 lines
5.7 KiB
Go
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
|
|
}
|