gomcp/internal/domain/pivot/engine.go

233 lines
5.5 KiB
Go

// Package pivot implements the autonomous multi-step attack engine (v3.8 Strike Force).
// Module 10 in Orchestrator: finite state machine for iterative offensive operations.
package pivot
import (
"fmt"
"sync"
"time"
)
// State represents the Pivot Engine FSM state.
type State uint8
const (
StateRecon State = iota // Reconnaissance: gather target info
StateHypothesis // Generate attack hypotheses
StateAction // Execute micro-exploit attempt
StateObserve // Analyze result of action
StateSuccess // Goal achieved
StateDeadEnd // Dead end → return to Hypothesis
)
// String returns the human-readable state name.
func (s State) String() string {
switch s {
case StateRecon:
return "RECON"
case StateHypothesis:
return "HYPOTHESIS"
case StateAction:
return "ACTION"
case StateObserve:
return "OBSERVE"
case StateSuccess:
return "SUCCESS"
case StateDeadEnd:
return "DEAD_END"
default:
return "UNKNOWN"
}
}
// StepResult captures the outcome of a single pivot step.
type StepResult struct {
StepNum int `json:"step_num"`
State State `json:"state"`
Action string `json:"action"`
Result string `json:"result"`
Timestamp time.Time `json:"timestamp"`
}
// Chain is a complete attack chain execution record.
type Chain struct {
Goal string `json:"goal"`
Steps []StepResult `json:"steps"`
FinalState State `json:"final_state"`
MaxAttempts int `json:"max_attempts"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
}
// DecisionRecorder records tamper-evident decisions.
type DecisionRecorder interface {
RecordDecision(module, decision, reason string)
}
// Engine is the Pivot Engine FSM (Module 10, v3.8).
// Executes multi-step attack chains with automatic backtracking
// on dead ends and configurable attempt limits.
type Engine struct {
mu sync.Mutex
state State
maxAttempts int
attempts int
recorder DecisionRecorder
chain *Chain
}
// Config holds Pivot Engine configuration.
type Config struct {
MaxAttempts int // Max total steps before forced termination (default: 50)
}
// DefaultConfig returns secure defaults.
func DefaultConfig() Config {
return Config{MaxAttempts: 50}
}
// NewEngine creates a new Pivot Engine.
func NewEngine(cfg Config, recorder DecisionRecorder) *Engine {
if cfg.MaxAttempts <= 0 {
cfg.MaxAttempts = 50
}
return &Engine{
state: StateRecon,
maxAttempts: cfg.MaxAttempts,
recorder: recorder,
}
}
// StartChain begins a new attack chain for the given goal.
func (e *Engine) StartChain(goal string) {
e.mu.Lock()
defer e.mu.Unlock()
e.state = StateRecon
e.attempts = 0
e.chain = &Chain{
Goal: goal,
MaxAttempts: e.maxAttempts,
StartedAt: time.Now(),
}
if e.recorder != nil {
e.recorder.RecordDecision("PIVOT", "CHAIN_START", fmt.Sprintf("goal=%s max=%d", goal, e.maxAttempts))
}
}
// Step advances the FSM by one step. Returns the result and whether the chain is complete.
func (e *Engine) Step(action, result string) (StepResult, bool) {
e.mu.Lock()
defer e.mu.Unlock()
e.attempts++
step := StepResult{
StepNum: e.attempts,
State: e.state,
Action: action,
Result: result,
Timestamp: time.Now(),
}
if e.chain != nil {
e.chain.Steps = append(e.chain.Steps, step)
}
// Record to decisions.log.
if e.recorder != nil {
e.recorder.RecordDecision("PIVOT", fmt.Sprintf("STEP_%s", e.state),
fmt.Sprintf("step=%d action=%s result=%s", e.attempts, action, truncate(result, 80)))
}
// Check termination conditions.
if e.attempts >= e.maxAttempts {
e.state = StateDeadEnd
e.finishChain()
return step, true
}
return step, false
}
// Transition moves the FSM to the next state based on current state and outcome.
func (e *Engine) Transition(success bool) {
e.mu.Lock()
defer e.mu.Unlock()
prev := e.state
switch e.state {
case StateRecon:
e.state = StateHypothesis
case StateHypothesis:
e.state = StateAction
case StateAction:
e.state = StateObserve
case StateObserve:
if success {
e.state = StateSuccess
} else {
e.state = StateDeadEnd
}
case StateDeadEnd:
// Backtrack to hypothesis generation.
e.state = StateHypothesis
case StateSuccess:
// Terminal state — no transition.
}
if e.recorder != nil && prev != e.state {
e.recorder.RecordDecision("PIVOT", "STATE_TRANSITION",
fmt.Sprintf("%s → %s (success=%v)", prev, e.state, success))
}
}
// Complete marks the chain as successfully completed.
func (e *Engine) Complete() {
e.mu.Lock()
defer e.mu.Unlock()
e.state = StateSuccess
e.finishChain()
}
// State returns the current FSM state.
func (e *Engine) CurrentState() State {
e.mu.Lock()
defer e.mu.Unlock()
return e.state
}
// Attempts returns the number of steps taken.
func (e *Engine) Attempts() int {
e.mu.Lock()
defer e.mu.Unlock()
return e.attempts
}
// GetChain returns the current chain record.
func (e *Engine) GetChain() *Chain {
e.mu.Lock()
defer e.mu.Unlock()
return e.chain
}
// IsTerminal returns true if the engine is in a terminal state.
func (e *Engine) IsTerminal() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.state == StateSuccess || (e.state == StateDeadEnd && e.attempts >= e.maxAttempts)
}
func (e *Engine) finishChain() {
if e.chain != nil {
e.chain.FinalState = e.state
e.chain.FinishedAt = time.Now()
}
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}