Documentation
¶
Overview ¶
Package purplecoverage is the control plane that closes the purple loop (#426): it joins the offensive half of the ledger (an emulation.Run's per-technique coverage records — what each technique executed and EXPECTED to be detected, #421) with the defensive half (the detections that ACTUALLY fired on the same asset in the run window, #422/#423) and resolves a per-technique coverage verdict through the pure domain. Coverage is measured from the two independent halves, never claimed.
The join and the verdict order (out_of_reach → unknown → covered → gap) live in the domain; this package supplies the two halves, persists the result tenant-scoped, and audits the computation. It is the same honest deferral as the rest of the blue-team pillar: the compute + store are real; the scheduler that triggers a run and streams its window is wired at the composition root later.
Index ¶
- type Result
- type Service
- func (s *Service) Compute(ctx context.Context, run emulation.Run, window Window) (Result, error)
- func (s *Service) Regressions(ctx context.Context, prevRun, currRun shared.ID) ([]purplecoverage.Regression, error)
- func (s *Service) Trend(ctx context.Context, engagementID shared.ID) ([]purplecoverage.Coverage, error)
- func (s *Service) WorkItems(ctx context.Context, engagementID, runID shared.ID) ([]purplecoverage.WorkItem, error)
- type Window
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Result ¶
type Result struct {
Coverage []purplecoverage.Coverage
Bonus []string
Gaps []purplecoverage.WorkItem
}
Result is one computation's honest outcome: the stored per-technique coverage, the bonus detections (fired but expected by no technique — reported, never hidden or counted as coverage), and the work items (one per gap).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service computes and serves purple coverage.
func NewService ¶
func NewService(store ports.PurpleCoverageStore, detections ports.DetectionRecordStore, audit ports.AuditLogger, clock ports.Clock) (*Service, error)
NewService validates its dependencies. All are required: without the detection ledger there is no defensive half to join against, and without the audit log a coverage computation would not be attributable.
func (*Service) Compute ¶
Compute joins an emulation run with the detections that fired on its asset in the window, resolves a verdict per technique, persists the coverage tenant-scoped, and audits the computation. The run's tenant/engagement must match the authenticated tenant on the context (fail closed) so a run cannot be scored into another tenant's ledger.
Every technique in the run is emulatable by construction (it was in the catalogue and produced a record); out_of_reach is a domain verdict reserved for techniques the platform cannot emulate, which a run does not carry — so a run resolves only to covered/gap/unknown, and a non-executed technique is unknown, never a gap.
func (*Service) Regressions ¶
func (s *Service) Regressions(ctx context.Context, prevRun, currRun shared.ID) ([]purplecoverage.Regression, error)
Regressions compares two runs' coverage and returns the techniques that went from covered to uncovered — a detection regression. Both runs are loaded tenant-scoped.
func (*Service) Trend ¶
func (s *Service) Trend(ctx context.Context, engagementID shared.ID) ([]purplecoverage.Coverage, error)
Trend returns an engagement's coverage across runs, oldest first, so a defender can see coverage improve (or regress) over time. Tenant-scoped through the store.
func (*Service) WorkItems ¶
func (s *Service) WorkItems(ctx context.Context, engagementID, runID shared.ID) ([]purplecoverage.WorkItem, error)
WorkItems returns one actionable item per gap in a run (the missing detection a human must write). It reads the stored coverage so it reflects what was measured, not a recomputation.
The run is bound to engagementID: ListByRun is only tenant-scoped, so a run id belonging to a DIFFERENT engagement in the same tenant would otherwise be readable through an engagement the caller is authorized for. Any record whose EngagementID does not match is dropped, so a mismatched run resolves to no work items rather than leaking another engagement's gaps.
type Window ¶
Window is the observation window a run's detections are joined over. It is explicit because an emulation.Run carries no timestamps: the caller (the scheduler that ran the emulation) owns when the run started and ended, and only detections observed inside [From,To] on the run's asset can count as coverage for that run. A detection outside the window is a different event, not this run's coverage.