validation

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 23, 2026 License: GPL-3.0 Imports: 7 Imported by: 0

Documentation

Overview

Package validation defines the Stage-4 contract: WHAT gets validated, WHAT counts as evidence, WHO gates it — but NOT HOW a technique runs.

Architectural note: OpenCTEM is agent-based. The API is an ORCHESTRATOR, not an executor. Actual exploit execution, cloud probes, and adversary emulation run on the agent that lives in the tenant's network, has tenant-local credentials, and can legally reach the target.

This package therefore holds:

  • Data shapes (TechniqueID, Target, Evidence, Outcome)
  • The attacker-profile gate (API-side policy)
  • The Dispatcher contract (API submits a job → agent executes → agent posts Evidence back via the ingest API)
  • EvidenceStore + Redactor (API persists, redacts secrets)

What this package does NOT hold:

  • Any direct call to AWS / Atomic Red Team / Caldera / Nuclei. Those belong to the agent repo.

Index

Constants

This section is empty.

Variables

View Source
var DefaultThresholds = CoverageThresholds{
	P0: 100,
	P1: 100,
	P2: 80,
	P3: 0,
}

DefaultThresholds matches the commitment: full coverage on P0/P1, 80% on P2, optional on P3.

View Source
var ErrCoverageBelowSLO = errors.New("validation coverage below SLO")

ErrCoverageBelowSLO is returned by Enforce when any class is under its threshold. Message enumerates the offending classes so the operator sees exactly which P0s are missing evidence.

View Source
var ErrNoExecutor = errors.New("no executor available")

ErrNoExecutor is returned by Selector.Select when nothing in the available list matches the policy.

Functions

func Enforce

Enforce returns nil when every class meets its threshold, else ErrCoverageBelowSLO wrapped with a human-readable breakdown. Used by the cycle-close handler: no close if coverage is under.

Types

type AgentCapability

type AgentCapability interface {
	AvailableExecutorKinds(ctx context.Context, tenantID shared.ID) ([]ExecutorKind, error)
}

AgentCapability lets the service ask "which ExecutorKinds are currently available for this tenant?" before selecting. A real implementation looks at agent registrations; the test stub returns a static slice.

type AttackerProfile

type AttackerProfile struct {
	ID           shared.ID
	Name         string
	Capabilities []string // "external-unauth" | "credentialed" | "network-pivot" | ...
}

AttackerProfile is the narrow subset of the full profile that the API-side selection / gating logic needs. It never travels to the agent — the agent receives the already-approved executor kind and technique.

type CoverageThresholds

type CoverageThresholds struct {
	P0 float64
	P1 float64
	P2 float64
	P3 float64
}

CoverageThresholds define the SLO targets per priority class. Exported so tenants can override later (per-tenant-config).

type DefaultSelector

type DefaultSelector struct{}

DefaultSelector applies a conservative mapping:

  • safe-check is preferred when available — cheapest + legally safest
  • nuclei is next for web-reachable targets
  • atomic-red-team / caldera require a non-empty profile capability set (waiver + adversary emulation opt-in)

func (DefaultSelector) Select

func (DefaultSelector) Select(
	tid TechniqueID,
	profile *AttackerProfile,
	available []ExecutorKind,
) (ExecutorKind, error)

Select picks the first available executor that fits the policy.

type Evidence

type Evidence struct {
	// ExecutorKind identifies which agent-side tool produced this
	// evidence. Enforced by the ingest handler against the
	// ExecutorKind declared on the job.
	ExecutorKind string
	Technique    TechniqueID
	Target       Target
	StartedAt    time.Time
	EndedAt      time.Time
	Outcome      Outcome
	Summary      string
	Artifacts    []string // attachment IDs (screenshots, PCAPs)
	RawMeta      map[string]any
}

Evidence is everything a reviewer needs to judge whether the technique was executed and what it produced. Produced by the agent and POSTed back through the validation-ingest endpoint.

func (Evidence) Executor deprecated

func (e Evidence) Executor() string

Executor is a back-compat accessor for legacy handler code that already referenced the field. Prefer ExecutorKind directly.

Deprecated: read ExecutorKind.

type EvidenceRepository

type EvidenceRepository interface {
	Create(ctx context.Context, ev StoredEvidence) error
	ListByFinding(ctx context.Context, tenantID, findingID shared.ID) ([]StoredEvidence, error)
}

EvidenceRepository persists StoredEvidence. Implemented by a postgres-backed type; tests use an in-memory fake.

type EvidenceStore

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

EvidenceStore is the app-layer facade. Calls redact → persist → returns the stored record so callers can surface it.

func NewEvidenceStore

func NewEvidenceStore(repo EvidenceRepository) *EvidenceStore

NewEvidenceStore wires defaults.

func (*EvidenceStore) ListForFinding

func (s *EvidenceStore) ListForFinding(
	ctx context.Context,
	tenantID, findingID shared.ID,
) ([]StoredEvidence, error)

ListForFinding returns every evidence record attached to a finding in chronological order. The UI uses this on the finding detail page.

func (*EvidenceStore) Record

func (s *EvidenceStore) Record(
	ctx context.Context,
	tenantID, findingID shared.ID,
	simulationRunID *shared.ID,
	ev Evidence,
) (StoredEvidence, error)

Record persists the evidence after redaction. Returns the stored envelope with ID populated. Errors from the repo are propagated.

type ExecutorKind

type ExecutorKind string

ExecutorKind enumerates the validation tool the AGENT will run. The API uses this string to route jobs to agents that declare they support it. The API does not import or call the tool itself.

const (
	KindSafeCheck     ExecutorKind = "safe-check"
	KindAtomicRedTeam ExecutorKind = "atomic-red-team"
	KindCaldera       ExecutorKind = "caldera"
	KindNuclei        ExecutorKind = "nuclei"
)

type FindingMutator

type FindingMutator interface {
	Get(ctx context.Context, tenantID, findingID shared.ID) (*vulnerability.Finding, error)
	Update(ctx context.Context, f *vulnerability.Finding) error
}

FindingMutator is the narrow contract for loading + saving a finding during the transition.

type Outcome

type Outcome string

Outcome is the exit status of an execution.

const (
	OutcomeDetected     Outcome = "detected"
	OutcomeNotDetected  Outcome = "not_detected"
	OutcomeInconclusive Outcome = "inconclusive"
	OutcomeError        Outcome = "error"
	OutcomeSkipped      Outcome = "skipped"
)

type ProofOfFixService

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

ProofOfFixService orchestrates the retest.

func NewProofOfFixService

func NewProofOfFixService(
	dispatcher ValidationDispatcher,
	capability AgentCapability,
	evStore *EvidenceStore,
	findingRepo FindingMutator,
	notifier RetestNotifier,
) *ProofOfFixService

NewProofOfFixService wires dependencies.

func (*ProofOfFixService) Retest

func (s *ProofOfFixService) Retest(
	ctx context.Context,
	tenantID, findingID shared.ID,
	tid TechniqueID,
	target Target,
	priorKind ExecutorKind,
	profile *AttackerProfile,
) (Evidence, bool, error)

Retest dispatches a validation job and reconciles the finding. Returns the Evidence the agent produced, a boolean indicating whether the fix stood (true = finding moved to resolved), and any error.

Passing priorKind routes to the same executor that produced the original validation when that kind is still available in the fleet. Falling back to Selector.Select when it is not.

type Redactor

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

Redactor scrubs common secret patterns from the evidence before it is persisted.

func NewRedactor

func NewRedactor() *Redactor

NewRedactor returns a Redactor with the default pattern set. Patterns are conservative — we would rather redact too much than too little. Operators can extend via AddPattern.

func (*Redactor) AddPattern

func (r *Redactor) AddPattern(re string)

AddPattern registers an additional regex. Panics on a bad regex (programmer error — regex is a const at the call site).

func (*Redactor) Redact

func (r *Redactor) Redact(ev Evidence) Evidence

Redact returns a copy of the evidence with secrets replaced by "[REDACTED]". Fields scrubbed:

  • Summary
  • RawMeta["stdout"], RawMeta["stderr"] (common places ART puts output)

type RetestNotifier

type RetestNotifier interface {
	NotifyFixRejected(ctx context.Context, tenantID, findingID shared.ID, reason string) error
}

RetestNotifier posts a message to the assignee when the retest refutes the fix. Nil notifier is acceptable — the status revert still happens.

type Selector

type Selector interface {
	Select(tid TechniqueID, profile *AttackerProfile, available []ExecutorKind) (ExecutorKind, error)
}

Selector owns the API-side policy: given a technique + attacker profile + a list of executor kinds the agent fleet supports, pick the appropriate kind.

type StoredEvidence

type StoredEvidence struct {
	ID              shared.ID
	TenantID        shared.ID
	FindingID       shared.ID  // the finding this execution validated
	SimulationRunID *shared.ID // optional; populated when evidence is part of a scheduled simulation
	Evidence        Evidence
	CreatedAt       time.Time
}

StoredEvidence is the persistence shape. Adds tenant_id + finding linkage to the in-memory Evidence struct. Persisted into the simulation_evidence table (migration lives in a companion PR).

type Target

type Target struct {
	AssetID  shared.ID
	Type     string // "host" | "web_url" | "api_endpoint" | "cloud_resource"
	Address  string // host:port, URL, ARN, etc.
	Metadata map[string]any
}

Target identifies what we are validating against. The executor kind determines which fields it uses.

type TechniqueID

type TechniqueID string

TechniqueID is the MITRE ATT&CK technique identifier. The API does not interpret it — it is passed to the agent unchanged.

type ValidationCoverage

type ValidationCoverage struct {
	P0Total        int
	P0WithEvidence int
	P1Total        int
	P1WithEvidence int
	P2Total        int
	P2WithEvidence int
	P3Total        int
	P3WithEvidence int
}

ValidationCoverage aggregates per-priority coverage for a tenant or cycle window.

func (ValidationCoverage) Pct

func (c ValidationCoverage) Pct(class string) float64

Pct returns the coverage ratio for a priority class in [0, 100]. Zero-total returns 100 (nothing to cover = trivially met).

type ValidationDispatcher

type ValidationDispatcher interface {
	Submit(ctx context.Context, job ValidationJob) (Evidence, error)
}

ValidationDispatcher submits a job for an agent and returns the resulting Evidence when the agent has reported back. Concrete implementations plug into the platform-job queue (Redis / Postgres).

Submit is expected to BLOCK until the agent finishes OR the context deadline fires — callers choose the deadline. In practice the implementation is queue + subscribe, not a synchronous call.

type ValidationJob

type ValidationJob struct {
	JobID          shared.ID
	TenantID       shared.ID
	FindingID      shared.ID
	ExecutorKind   ExecutorKind
	Technique      TechniqueID
	Target         Target
	ProfileID      shared.ID
	TimeoutSeconds int
}

ValidationJob is the payload the API queues for an agent. Agents long-poll for jobs that match their advertised ExecutorKinds. Result is delivered via POST /api/v1/validation/evidence.

Jump to

Keyboard shortcuts

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