Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
133
server/internal/vuln/correlator.go
Normal file
133
server/internal/vuln/correlator.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package vuln
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Finding mirrors agent vuln_findings JSON for server-side enrichment.
|
||||
type Finding struct {
|
||||
CVEID string `json:"cve_id"`
|
||||
Severity string `json:"severity"`
|
||||
Component string `json:"component"`
|
||||
Patched bool `json:"patched"`
|
||||
ExploitableInFleetContext bool `json:"exploitable_in_fleet_context"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// CatalogEntry is a lightweight embedded CVE rule for fleet correlator.
|
||||
type CatalogEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Component string `json:"component"`
|
||||
Severity string `json:"severity"`
|
||||
FleetPorts []int `json:"fleet_ports,omitempty"`
|
||||
PatchKBs []string `json:"patch_kbs,omitempty"`
|
||||
}
|
||||
|
||||
// EmbeddedCatalog is served by GET /api/v1/vuln/catalog (cached JSON).
|
||||
var EmbeddedCatalog = []CatalogEntry{
|
||||
{ID: "CVE-2021-44228", Name: "Log4Shell", Component: "Apache Log4j", Severity: "critical"},
|
||||
{ID: "CVE-2021-26855", Name: "ProxyLogon", Component: "Microsoft Exchange", Severity: "critical", FleetPorts: []int{443, 80}, PatchKBs: []string{"KB5000871"}},
|
||||
{ID: "CVE-2020-1472", Name: "Zerologon", Component: "Microsoft Netlogon", Severity: "critical", FleetPorts: []int{445, 135}, PatchKBs: []string{"KB4577015"}},
|
||||
{ID: "CVE-2019-19781", Name: "Citrix ADC", Component: "Citrix ADC/Gateway", Severity: "critical", FleetPorts: []int{443}},
|
||||
{ID: "CVE-2019-11510", Name: "Pulse Secure", Component: "Ivanti Pulse Connect Secure", Severity: "critical", FleetPorts: []int{443}},
|
||||
{ID: "CVE-2020-5902", Name: "F5 BIG-IP", Component: "F5 BIG-IP", Severity: "critical", FleetPorts: []int{443, 8443}},
|
||||
{ID: "CVE-2022-1388", Name: "F5 iControl", Component: "F5 BIG-IP", Severity: "critical", FleetPorts: []int{443, 8443}},
|
||||
{ID: "CVE-2021-26084", Name: "Confluence OGNL", Component: "Atlassian Confluence", Severity: "critical", FleetPorts: []int{8090, 8443}},
|
||||
{ID: "CVE-2022-26134", Name: "Confluence RCE", Component: "Atlassian Confluence", Severity: "critical", FleetPorts: []int{8090, 8443}},
|
||||
{ID: "CVE-2021-40539", Name: "ManageEngine", Component: "Zoho ManageEngine ADSelfService Plus", Severity: "critical", FleetPorts: []int{9251}},
|
||||
{ID: "CVE-2018-13379", Name: "FortiOS path traversal", Component: "Fortinet FortiGate/FortiOS", Severity: "critical", FleetPorts: []int{443, 10443}},
|
||||
{ID: "CVE-2021-34527", Name: "PrintNightmare", Component: "Windows Print Spooler", Severity: "high", FleetPorts: []int{445, 135}, PatchKBs: []string{"KB5004945"}},
|
||||
{ID: "CVE-2020-0688", Name: "Exchange RCE", Component: "Microsoft Exchange", Severity: "high", FleetPorts: []int{443}},
|
||||
{ID: "CVE-2021-21972", Name: "vCenter RCE", Component: "VMware vCenter", Severity: "critical", FleetPorts: []int{443}},
|
||||
}
|
||||
|
||||
// FleetContext carries server-known signals for correlator enrichment.
|
||||
type FleetContext struct {
|
||||
ListenPortCount int
|
||||
PathTracerPorts []int
|
||||
SSHAvailable bool
|
||||
OSVersion string
|
||||
}
|
||||
|
||||
// EnrichFindings applies fleet-context rules (open ports from Path Tracer when available).
|
||||
func EnrichFindings(findings []Finding, ctx FleetContext) []Finding {
|
||||
if len(findings) == 0 {
|
||||
return findings
|
||||
}
|
||||
portSet := make(map[int]bool)
|
||||
for _, p := range ctx.PathTracerPorts {
|
||||
portSet[p] = true
|
||||
}
|
||||
rules := catalogByID()
|
||||
out := make([]Finding, len(findings))
|
||||
copy(out, findings)
|
||||
for i := range out {
|
||||
if out[i].Patched || out[i].ExploitableInFleetContext {
|
||||
continue
|
||||
}
|
||||
rule, ok := rules[out[i].CVEID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, p := range rule.FleetPorts {
|
||||
if portSet[p] {
|
||||
out[i].ExploitableInFleetContext = true
|
||||
out[i].Detail = strings.TrimSpace(out[i].Detail + " · Path Tracer hop port " + itoa(p) + " open")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !out[i].ExploitableInFleetContext && ctx.SSHAvailable && ctx.ListenPortCount > 0 {
|
||||
for _, p := range rule.FleetPorts {
|
||||
if p == 22 || p == 445 || p == 443 {
|
||||
out[i].ExploitableInFleetContext = true
|
||||
out[i].Detail = strings.TrimSpace(out[i].Detail + " · fleet SSH/listener context")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func catalogByID() map[string]CatalogEntry {
|
||||
m := make(map[string]CatalogEntry, len(EmbeddedCatalog))
|
||||
for _, e := range EmbeddedCatalog {
|
||||
m[e.ID] = e
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [12]byte
|
||||
i := len(b)
|
||||
for n > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
|
||||
// RiskScore computes a 0-100 score from enriched findings.
|
||||
func RiskScore(findings []Finding) int {
|
||||
score := 0
|
||||
for _, f := range findings {
|
||||
if f.ExploitableInFleetContext {
|
||||
if f.Severity == "critical" {
|
||||
score += 25
|
||||
} else {
|
||||
score += 12
|
||||
}
|
||||
} else if !f.Patched {
|
||||
score += 8
|
||||
}
|
||||
}
|
||||
if score > 100 {
|
||||
return 100
|
||||
}
|
||||
return score
|
||||
}
|
||||
30
server/internal/vuln/correlator_test.go
Normal file
30
server/internal/vuln/correlator_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package vuln
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEnrichFindingsPathTracerPort(t *testing.T) {
|
||||
in := []Finding{{
|
||||
CVEID: "CVE-2021-26855", Severity: "critical", Component: "Exchange",
|
||||
Patched: false, ExploitableInFleetContext: false,
|
||||
}}
|
||||
out := EnrichFindings(in, FleetContext{PathTracerPorts: []int{443}})
|
||||
if len(out) != 1 || !out[0].ExploitableInFleetContext {
|
||||
t.Fatalf("expected fleet exploitability with 443 from pathtracer, got %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRiskScore(t *testing.T) {
|
||||
s := RiskScore([]Finding{
|
||||
{Severity: "critical", ExploitableInFleetContext: true},
|
||||
{Severity: "high", Patched: false},
|
||||
})
|
||||
if s < 25 {
|
||||
t.Fatalf("expected elevated score, got %d", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedCatalogNotEmpty(t *testing.T) {
|
||||
if len(EmbeddedCatalog) < 10 {
|
||||
t.Fatalf("expected catalog entries")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user