exploitation

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package exploitation is the evidence-gated lifecycle for AI/exploitation findings. An AI finding is a CLAIM until an independent adversarial verifier proves it. Two operations:

Propose creates the finding at EvidenceScore 0 (KindExploitation). Until proven it can be neither confirmed (CanPromote == false) nor included in a report. Confirm applies an adversarial-verifier verdict. It SEALS the verdict into the evidence chain FIRST (fail-closed), then raises the score – so a raised score ALWAYS has a sealed, hash-chained "try to refute" record behind it. The proposing agent has no path to raise its own score: Confirm is not an agent-callable tool (the catalog exposes no write/verdict tool), and there is no other evidence-score setter in the use cases.

Index

Constants

View Source
const SimulationVerifier = "synapse-simulation-verifier"

SimulationVerifier is the fixed, system-owned verifier principal a simulated chain records. It is distinct from any human step proposer, so the Machine's distinct-verifier rule (a proposer cannot confirm its own step) holds without a second human in the loop.

View Source
const VerdictEvidenceKind = "exploitation_verdict"

VerdictEvidenceKind is the evidence-chain kind under which a sealed adversarial verdict is recorded (the provenance behind any raised score).

Variables

View Source
var ErrChainNotAdvanceable = errors.New("a chain step advances only on a distinct verifier's sealed verdict")

ErrChainNotAdvanceable is returned if a caller tries to advance a chain through any path other than a verified step. It exists so a test can assert that there is no back door.

Functions

This section is empty.

Types

type ChainRegistry added in v0.1.8

type ChainRegistry struct {
	// contains filtered or unexported fields
}

ChainRegistry tracks the exploitation chains currently running IN THIS PROCESS, so the offensive kill switch (issue #418, follow-up on #420) can reach them.

The work-order layer of the kill switch cancels queued and claimed offensive work orders. A chain already executing in memory is not a work order, so without this registry a running chain would be the one thing an operator's halt could not stop. The registry closes that gap: a Machine registers itself while it runs and the kill switch halts every registered chain for the tenant.

Scope honesty. This registry covers the process it lives in. In the current single-process deployment that is the whole estate's control plane. Under horizontal scale (P5) a chain runs on one node and a fleet-wide halt must fan out to every node's registry — a P5 concern this type does not pretend to solve. It never claims to reach a chain running in another process.

It implements offensivepolicyuc.ChainHalter. The interface is defined in the offensivepolicy package (the consumer) and implemented here; the dependency points from exploitation to offensivepolicy, the same direction admission already uses, so there is no import cycle.

func NewChainRegistry added in v0.1.8

func NewChainRegistry() *ChainRegistry

NewChainRegistry constructs an empty registry.

func (*ChainRegistry) HaltChains added in v0.1.8

func (r *ChainRegistry) HaltChains(ctx context.Context, tenantID shared.ID, actor, reason string) (offensivepolicyuc.ChainHaltSummary, error)

HaltChains halts every chain this process is running for the tenant, runs each one's cleanup, and reports the outcome. It satisfies offensivepolicyuc.ChainHalter.

A halted chain reaches a terminal state and unregisters itself here, so a second halt is a no-op. A chain that FAILS to halt (cleanup error) stays registered and is reported in Failed — a human must look, and a retry must not silently spin on it.

A chain that registers WHILE a halt is in progress is still caught: the halt re-scans until no haltable chain remains, stopping only when a pass makes no progress (everything left is a recorded failure). This bounds the work to the chains that actually exist and cannot livelock.

func (*ChainRegistry) Register added in v0.1.8

func (r *ChainRegistry) Register(m *Machine)

Register records a running chain under its tenant. It is idempotent and safe for concurrent use.

func (*ChainRegistry) RunTracked added in v0.1.8

func (r *ChainRegistry) RunTracked(ctx context.Context, m *Machine) (dexploit.ChainState, error)

RunTracked registers the machine, runs it to a terminal state, and unregisters it — even if Run panics or returns an error. Any goroutine that drives a chain should use this so the chain is haltable for exactly as long as it is running, and never leaks into the registry afterwards.

func (*ChainRegistry) Unregister added in v0.1.8

func (r *ChainRegistry) Unregister(tenant, chainID shared.ID)

Unregister drops a chain from the registry (its run finished or it was halted). It is idempotent.

type ChainStore added in v0.1.8

type ChainStore interface {
	SaveChain(ctx context.Context, chain *dexploit.Chain) error
}

ChainStore persists a chain's state as it advances, so a halt or a crash cannot lose track of which steps still hold a cleanup obligation.

type CleanupRunner added in v0.1.8

type CleanupRunner interface {
	Cleanup(ctx context.Context, chain *dexploit.Chain, step dexploit.Step) error
}

CleanupRunner undoes a state-changing step. It runs on halt and on failure, in reverse order of execution, for every completed state-changing step.

type CreatedFindingProjector added in v0.2.0

type CreatedFindingProjector interface {
	ProjectCreatedFinding(context.Context, shared.ID, finding.Finding, string) error
}

type FindingStore

type FindingStore interface {
	Upsert(ctx context.Context, findings []finding.Finding) error
	ListByEngagement(ctx context.Context, engagementID shared.ID) ([]finding.Finding, error)
	SetEvidenceScore(ctx context.Context, engagementID, findingID shared.ID, score, expectedVersion int) (finding.Finding, error)
}

FindingStore is the narrow slice of the finding repository this use case needs. The evidence-score setter is intentionally NOT on the broad ports.FindingRepository – it lives only on the concrete repos and is reached only here, so a read-only consumer (the agent tool catalog) cannot move a score. The concrete repos satisfy this consumer interface.

type Machine added in v0.1.8

type Machine struct {
	// contains filtered or unexported fields
}

Machine orchestrates one chain. It is a Go state machine over the pure domain chain: it admits, executes, seals, verifies and advances, and it runs cleanup on any halt or failure. No agent tool is registered that reaches Run or Halt, so a model can propose a chain but never drives one.

func NewMachine added in v0.1.8

func NewMachine(chain *dexploit.Chain, admit StepAdmitter, exec StepExecutor, v StepVerifier, cleanup CleanupRunner, sealer stepSealer, store ChainStore, audit ports.AuditLogger, clock ports.Clock) (*Machine, error)

NewMachine validates its dependencies. Every one is required: a chain that cannot admit, seal, verify, clean up or audit is not a governed chain, and a partial one would be worse than none because it would look governed.

func (*Machine) Chain added in v0.1.8

func (m *Machine) Chain() *dexploit.Chain

Chain exposes the underlying chain for inspection (read-only intent). It returns the live pointer and does NOT take the machine lock, so it is safe only before Run starts or after it returns — never concurrently with a running Run or a Halt. Use State() for a lock-guarded read while a chain is live.

func (*Machine) Halt added in v0.1.8

func (m *Machine) Halt(ctx context.Context, actor, reason string) (dexploit.ChainState, error)

Halt is the kill switch's entry point for a running chain: stop, run cleanup for every completed state-changing step, and audit. It returns the terminal state. It takes the machine lock, so a Halt racing Run interposes at a step boundary and sees a consistent chain.

func (*Machine) Run added in v0.1.8

Run drives the chain to a terminal state: step by step, admit → execute → seal → verify → advance, running cleanup on any halt or failure. It returns the terminal state.

A refusal at any step (admission, blast-radius violation, missing evidence, sub-bar or self verdict) halts the chain and triggers cleanup rather than continuing. Run is idempotent on a terminal chain.

func (*Machine) State added in v0.1.8

func (m *Machine) State() dexploit.ChainState

State returns the current chain state. It takes the machine lock so a caller can read the state while Run is executing on another goroutine (e.g. the kill-switch registry deciding what to halt).

type PolicyStepAdmitter added in v0.1.8

type PolicyStepAdmitter struct {
	// contains filtered or unexported fields
}

PolicyStepAdmitter admits each chain step through the #418 offensive governance policy, individually, carrying the engagement's rules of engagement and the recorded approvals for that step.

It satisfies StepAdmitter. Every step is a fresh Authorize call evaluated at the current time — a chain never inherits permission from its first step, so a window that closes mid-chain, or a step whose technique is not in the register, halts the chain at exactly that step.

func NewPolicyStepAdmitter added in v0.1.8

func NewPolicyStepAdmitter(gov governanceAuthorizer, roe offensivepolicyuc.RulesOfEngagement, approvals StepApprovals, now func() time.Time) *PolicyStepAdmitter

NewPolicyStepAdmitter wires the governance service, the engagement's rules of engagement, an approval resolver, and a clock into a StepAdmitter. now defaults to time.Now when nil.

func (*PolicyStepAdmitter) AdmitStep added in v0.1.8

func (a *PolicyStepAdmitter) AdmitStep(ctx context.Context, chain *dexploit.Chain, step dexploit.Step) error

AdmitStep authorizes one step. The Machine calls this before the step executes.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service runs the evidence-gated exploitation-finding lifecycle.

func NewService

func NewService(findings FindingStore, ev evidenceSealer, audit ports.AuditLogger, clock ports.Clock, ids ports.IDGenerator) (*Service, error)

NewService validates its dependencies (all required; the evidence sealer is mandatory because a verdict that cannot be sealed must never move a score).

func (*Service) Confirm

func (s *Service) Confirm(ctx context.Context, verifier string, engagementID, findingID shared.ID, score int, rationale string, expectedVersion int) (finding.Finding, error)

Confirm applies an adversarial-verifier verdict to a proposed finding. It seals the verdict as evidence FIRST (fail-closed: an unrecorded verdict never moves a score), then sets the finding's EvidenceScore under optimistic concurrency (expectedVersion guards a racing update). A passing score (>= EvidenceThreshold) makes the finding promotable/reportable; a refuting (low) score leaves it gated. Returns the updated finding.

Provenance is one-directional by design: a moved score ALWAYS has a sealed verdict behind it, but the converse does not strictly hold – because the seal precedes the version-guarded write, a Confirm that LOSES a concurrency race (ErrConflict) can leave an orphan sealed verdict in the append-only chain with no score move. That is acceptable: the verdict is a real adversarial assessment that happened; it simply did not win the race.

func (*Service) Propose

func (s *Service) Propose(ctx context.Context, proposer string, engagementID shared.ID, in finding.ExploitationInput) (finding.Finding, error)

Propose creates an AI/exploitation finding at EvidenceScore 0. proposer is the actor that proposed it (an agent session – attribution only; it confers no power to confirm).

func (*Service) ProposeHypothesis

func (s *Service) ProposeHypothesis(ctx context.Context, proposer string, engagementID shared.ID, in finding.HypothesisInput) (finding.Finding, error)

ProposeHypothesis creates an AI attack-chain hypothesis finding (Kind=hypothesis) at EvidenceScore 0, linking the named constituent findings. proposer is the agent session (attribution only – the finding is gated and confers no power to verify; a DISTINCT human raises the score via Confirm). Mirrors Propose; NewHypothesis validates (>= 2 constituents, title+description, etc.) and stamps the gating ProposedBy.

func (*Service) SetAttributor added in v0.1.8

func (s *Service) SetAttributor(a ports.FindingAttributor)

SetAttributor wires explicit producer-owned bindings; nil preserves deployments without fleet assets.

func (*Service) SetLifecycleShadow added in v0.2.0

func (s *Service) SetLifecycleShadow(tx ports.TenantTransactionRunner, projector CreatedFindingProjector, enabled func(string) bool, engagements ports.EngagementTenantResolver) error

SetLifecycleShadow makes a newly proposed offensive Finding, its attribution, audit entry, and native lineage projection one tenant-local transaction.

type SimulationCleanupRunner added in v0.2.0

type SimulationCleanupRunner struct{}

SimulationCleanupRunner discharges a step's cleanup obligation without touching a host, since the simulation changed nothing. It keeps the chain-of-custody consistent (every state-changing step's cleanup is recorded as run) without performing real teardown.

func (SimulationCleanupRunner) Cleanup added in v0.2.0

type SimulationExecutor added in v0.2.0

type SimulationExecutor struct{}

SimulationExecutor is a StepExecutor that EXECUTES NOTHING on any host. It exists so the full governed offensive flow — admission through the #418 policy, single-step blast-radius enforcement, distinct-verifier sealing, and cleanup — can be wired and exercised end-to-end WITHOUT crossing the execution-safety boundary. It mirrors responseuc.SimulationExecutor and is shared by adversary emulation and exploitation chains.

It echoes the step's declared blast radius as the observed radius, so a benign, in-radius step records as cleanly executed and the Machine's radiusExceeded guard sees no escalation. It never performs I/O.

A REAL host executor is DELIBERATELY not provided. Running a technique argv on a live endpoint is a hard-to-reverse outward action: it must go through the same argv-only sandbox as every other tool, and wiring it requires a distinct execution-safety review plus explicit operator authorization. Until then the simulation executor keeps the governed chain-of-custody honest and testable without ever touching a host, which is the platform's stated differentiator (proof under a defensible chain of custody).

func (SimulationExecutor) Execute added in v0.2.0

Execute performs no host action. It reports the declared radius as observed (no escalation), a benign observation, and success, so a governed step advances through the chain-of-custody without host contact.

type SimulationStepVerifier added in v0.2.0

type SimulationStepVerifier struct{}

SimulationStepVerifier confirms a SIMULATED step. It attests that the rehearsal ran within the step's declared blast radius, NOT that a real exploit succeeded: the executor touched no host, the sealed evidence and the run are labeled simulation, and the verdict rationale says so. It exists so the governed Machine (admission, sealed evidence, distinct verification, cleanup) can run end to end as a rehearsal. A real, independent verifier is the deliberate extension point that a live host executor requires.

func (SimulationStepVerifier) Verify added in v0.2.0

Verify returns a distinct-verifier verdict for the simulated outcome. A rehearsed step clears the evidence bar; a step the simulation could not complete does not.

type StepAdmitter added in v0.1.8

type StepAdmitter interface {
	AdmitStep(ctx context.Context, chain *dexploit.Chain, step dexploit.Step) error
}

StepAdmitter admits one step through the offensive governance policy and the execution guard before it runs. It returns an error (ErrForbidden) when the step's technique is not permitted for the target, the window has closed, or approval is missing. Each step is admitted INDIVIDUALLY — a chain never inherits permission from its first step.

type StepApprovals added in v0.1.8

type StepApprovals func(step dexploit.Step) []offensivepolicyuc.Approval

StepApprovals resolves the recorded human approvals for one step. It is a function so the caller can source approvals from wherever they live without this package depending on that store.

type StepExecutor added in v0.1.8

type StepExecutor interface {
	Execute(ctx context.Context, chain *dexploit.Chain, step dexploit.Step) (StepOutcome, error)
}

StepExecutor runs one step argv-only inside the sandbox with egress scoped to the authorised target. The Machine never shells out or opens a socket itself; everything a step does goes through here.

type StepOutcome added in v0.1.8

type StepOutcome struct {
	Succeeded      bool
	ObservedRadius offensivepolicy.Radius
	Proof          []byte // raw; the Machine bounds and redacts it before sealing
	Observation    string // the specific observation that proves success/failure, for the evidence record
}

StepOutcome is what the sandboxed executor reports back. ObservedRadius is the effect that ACTUALLY occurred, which the Machine checks against the step's declared radius: a step that changed state while declaring read_only is a policy violation, not a success.

type StepVerifier added in v0.1.8

type StepVerifier interface {
	Verify(ctx context.Context, chain *dexploit.Chain, step dexploit.Step, outcome StepOutcome) (verdict.Verdict, error)
}

StepVerifier produces a DISTINCT verifier's sealed verdict for a step's outcome. Its verdict's Verifier must differ from the step's proposer; the Machine enforces that and refuses otherwise, so a model cannot talk itself through a chain by verifying its own step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL