feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence
Extend owned-fleet control with scheduled tasks, audit log, file browser, HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
This commit is contained in:
231
agent/client/cve_scan_windows.go
Normal file
231
agent/client/cve_scan_windows.go
Normal file
@@ -0,0 +1,231 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const kevProbeScript = `
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
$out = [ordered]@{}
|
||||
|
||||
# Exchange (ProxyLogon / ProxyLogon family)
|
||||
$exSvc = @(Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'MSExchange*' -or $_.DisplayName -like '*Exchange*' })
|
||||
$exReg = Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer'
|
||||
$out.exchange_installed = ($exSvc.Count -gt 0 -or $exReg)
|
||||
|
||||
# Domain Controller (Zerologon surface)
|
||||
try {
|
||||
$dc = (Get-CimInstance Win32_ComputerSystem).DomainRole -in 4,5
|
||||
} catch { $dc = $false }
|
||||
$out.is_domain_controller = $dc
|
||||
|
||||
# Pulse / Ivanti VPN client or service
|
||||
$pulse = @(Get-Service -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.DisplayName -match 'Pulse|Ivanti|Juniper Pulse' -or $_.Name -match 'Pulse'
|
||||
})
|
||||
$out.pulse_present = ($pulse.Count -gt 0)
|
||||
|
||||
# Citrix ADC / Gateway / Workspace server components
|
||||
$citrix = @(
|
||||
Test-Path 'C:\inetpub\scripts',
|
||||
(Test-Path 'C:\Program Files\Citrix'),
|
||||
(Test-Path 'C:\Program Files (x86)\Citrix')
|
||||
) | Where-Object { $_ }
|
||||
$out.citrix_present = ($citrix.Count -gt 0)
|
||||
|
||||
# F5 BIG-IP local management (rare on desktop)
|
||||
$f5 = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'bigip|f5' })
|
||||
$out.f5_process = ($f5.Count -gt 0)
|
||||
|
||||
# Confluence / Atlassian stack
|
||||
$conf = @(Get-Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -match 'atlassian|confluence|tomcat' -or $_.ProcessName -match 'confluence|tomcat'
|
||||
})
|
||||
$out.confluence_like = ($conf.Count -gt 0)
|
||||
|
||||
# ManageEngine ADSelfService Plus
|
||||
$me = @(
|
||||
Test-Path 'C:\Program Files\ManageEngine',
|
||||
Test-Path 'C:\ManageEngine'
|
||||
) | Where-Object { $_ }
|
||||
$out.manageengine_present = ($me.Count -gt 0)
|
||||
|
||||
# Fortinet FortiClient
|
||||
$forti = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'forti' })
|
||||
$out.forticlient = ($forti.Count -gt 0)
|
||||
|
||||
# VMware vCenter / vSphere client heavy installs
|
||||
$vmw = @(Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'vpxd|VMware' })
|
||||
$out.vmware_serverish = ($vmw.Count -gt 0)
|
||||
|
||||
# Print Spooler (PrintNightmare surface)
|
||||
try {
|
||||
$sp = Get-Service Spooler
|
||||
$out.spooler_running = ($sp.Status -eq 'Running')
|
||||
} catch { $out.spooler_running = $false }
|
||||
|
||||
# Log4j jars — shallow search (bounded)
|
||||
$log4j = @()
|
||||
$roots = @(
|
||||
$env:ProgramFiles,
|
||||
${env:ProgramFiles(x86)},
|
||||
'C:\ProgramData'
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
foreach ($root in $roots) {
|
||||
$log4j += Get-ChildItem -Path $root -Filter 'log4j-core*.jar' -Recurse -Depth 3 -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 5 -ExpandProperty FullName
|
||||
}
|
||||
$out.log4j_jars = @($log4j | Select-Object -Unique)
|
||||
|
||||
$out | ConvertTo-Json -Compress -Depth 4
|
||||
`
|
||||
|
||||
type kevProbeResult struct {
|
||||
ExchangeInstalled bool `json:"exchange_installed"`
|
||||
IsDomainController bool `json:"is_domain_controller"`
|
||||
PulsePresent bool `json:"pulse_present"`
|
||||
CitrixPresent bool `json:"citrix_present"`
|
||||
F5Process bool `json:"f5_process"`
|
||||
ConfluenceLike bool `json:"confluence_like"`
|
||||
ManageEnginePresent bool `json:"manageengine_present"`
|
||||
FortiClient bool `json:"forticlient"`
|
||||
VMwareServerish bool `json:"vmware_serverish"`
|
||||
SpoolerRunning bool `json:"spooler_running"`
|
||||
Log4jJars []string `json:"log4j_jars"`
|
||||
}
|
||||
|
||||
func runKEVProbe() (*kevProbeResult, error) {
|
||||
out, err := silentCombinedOutput(
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command",
|
||||
kevProbeScript,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
if idx := strings.LastIndex(raw, "{"); idx > 0 {
|
||||
raw = raw[idx:]
|
||||
}
|
||||
var p kevProbeResult
|
||||
if err := json.Unmarshal([]byte(raw), &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func scanKEVExposure(patch *PatchStatusReport, ports *ListenPortsReport, sec *SysCheckSecurity) *KEVScanReport {
|
||||
probe, probeErr := runKEVProbe()
|
||||
findings := make([]KEVFinding, 0, len(KEVCatalog))
|
||||
|
||||
patchDays := -1
|
||||
if patch != nil && patch.LastPatchDays != nil {
|
||||
patchDays = *patch.LastPatchDays
|
||||
}
|
||||
listening := map[int]bool{}
|
||||
if ports != nil {
|
||||
for _, p := range ports.Ports {
|
||||
listening[p.Port] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range KEVCatalog {
|
||||
f := KEVFinding{
|
||||
CVE: e.ID, Name: e.Name, Product: e.Product, Severity: e.Severity, CISAKEV: e.CISAKEV,
|
||||
Status: "clear", Detail: e.Description,
|
||||
}
|
||||
if probeErr != nil {
|
||||
f.Status = "n/a"
|
||||
f.Detail = "probe unavailable"
|
||||
findings = append(findings, f)
|
||||
continue
|
||||
}
|
||||
|
||||
switch e.ID {
|
||||
case "CVE-2021-26855", "CVE-2020-0688":
|
||||
if probe.ExchangeInstalled {
|
||||
f.Status = "exposed"
|
||||
f.Detail = "Microsoft Exchange services/registry detected — verify Mar 2021+ CU patches"
|
||||
if patchDays >= 0 && patchDays > 90 {
|
||||
f.Status = "likely"
|
||||
f.Detail += "; host patch age > 90 days"
|
||||
}
|
||||
}
|
||||
case "CVE-2020-1472":
|
||||
if probe.IsDomainController {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Domain controller role — ensure Aug 2020 Netlogon patch (Zerologon) applied"
|
||||
if patchDays >= 0 && patchDays > 60 {
|
||||
f.Status = "exposed"
|
||||
f.Detail = "DC with patch age > 60 days — Zerologon mitigation urgency"
|
||||
}
|
||||
}
|
||||
case "CVE-2021-44228":
|
||||
if len(probe.Log4jJars) > 0 {
|
||||
f.Status = "likely"
|
||||
f.Detail = "log4j-core JAR(s) found: " + strings.Join(probe.Log4jJars, "; ")
|
||||
}
|
||||
case "CVE-2019-19781":
|
||||
if probe.CitrixPresent {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Citrix install paths present — verify ADC/Gateway patch level if server role"
|
||||
}
|
||||
case "CVE-2019-11510":
|
||||
if probe.PulsePresent {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Pulse/Ivanti VPN software detected — verify appliance firmware if VPN gateway"
|
||||
}
|
||||
case "CVE-2020-5902", "CVE-2022-1388":
|
||||
if probe.F5Process || listening[443] {
|
||||
if probe.F5Process {
|
||||
f.Status = "likely"
|
||||
f.Detail = "F5-related process detected"
|
||||
}
|
||||
}
|
||||
case "CVE-2021-26084", "CVE-2022-26134":
|
||||
if probe.ConfluenceLike {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Atlassian/Confluence-like Java process — verify Confluence patch level"
|
||||
}
|
||||
case "CVE-2021-40539":
|
||||
if probe.ManageEnginePresent {
|
||||
f.Status = "likely"
|
||||
f.Detail = "ManageEngine directory present — verify ADSelfService Plus version"
|
||||
}
|
||||
case "CVE-2018-13379":
|
||||
if probe.FortiClient {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Fortinet client process running — verify FortiOS/FortiClient versions on VPN edge"
|
||||
}
|
||||
case "CVE-2021-21972":
|
||||
if probe.VMwareServerish {
|
||||
f.Status = "likely"
|
||||
f.Detail = "VMware server-style services detected — verify vCenter patch level"
|
||||
}
|
||||
case "CVE-2021-34527":
|
||||
if probe.SpoolerRunning && !probe.IsDomainController {
|
||||
f.Status = "likely"
|
||||
f.Detail = "Print Spooler running — restrict if not required (PrintNightmare era)"
|
||||
}
|
||||
}
|
||||
|
||||
// Stale patching amplifies any likely/exposed KEV surface
|
||||
if f.Status == "likely" && patchDays > 120 {
|
||||
f.Detail += " · OS patches older than 120 days"
|
||||
}
|
||||
|
||||
findings = append(findings, f)
|
||||
}
|
||||
|
||||
r := finalizeKEVReport(findings)
|
||||
if r.ExposedCount > 0 || r.CriticalCount > 0 {
|
||||
r.Summary = "CISA KEV-style exposure indicators detected — patch or isolate affected roles"
|
||||
} else if r.LikelyCount > 0 {
|
||||
r.Summary = "Some KEV-related software stacks detected — verify versions and patches"
|
||||
} else {
|
||||
r.Summary = "No high-confidence KEV exposure indicators on this host"
|
||||
}
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user