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 ¶
- Variables
- func Enforce(c ValidationCoverage, t CoverageThresholds) error
- type AgentCapability
- type AttackerProfile
- type CoverageThresholds
- type DefaultSelector
- type Evidence
- func (e Evidence) Executor() stringdeprecated
- type EvidenceRepository
- type EvidenceStore
- type ExecutorKind
- type FindingMutator
- type Outcome
- type ProofOfFixService
- type Redactor
- type RetestNotifier
- type Selector
- type StoredEvidence
- type Target
- type TechniqueID
- type ValidationCoverage
- type ValidationDispatcher
- type ValidationJob
Constants ¶
This section is empty.
Variables ¶
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.
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.
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 ¶
func Enforce(c ValidationCoverage, t CoverageThresholds) error
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 ¶
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.
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 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 ¶
AddPattern registers an additional regex. Panics on a bad regex (programmer error — regex is a const at the call site).
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.