Files
AetherForge/agent/vulnprobe/scan.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
}