mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 04:16:22 +02:00
233 lines
5.5 KiB
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] + "..."
|
|
}
|