Documentation
¶
Overview ¶
Package engine orchestrates a run: it expands a Saga into scan jobs (controllers × components), executes them with bounded parallelism, and aggregates each control's results. Content-hash caching and the full describe→publish pipeline build on this.
Index ¶
- func Waivable(control string) bool
- type Engine
- type Option
- func WithAllowedEffects(kinds []string) Option
- func WithCache(c cache.Cache) Option
- func WithCacheableTarget(fn func(plugin.Target) bool) Option
- func WithConcurrency(n int) Option
- func WithPrioritization(p Prioritizer) Option
- func WithRemoteResolver(r RemoteResolver) Option
- func WithSBOM(g sbom.Generator) Option
- func WithScope(sc Scope) Option
- func WithWorkingTree() Option
- func WithoutPrewarm() Option
- type PlannedJob
- type Prioritizer
- type Priority
- type Registry
- func (r *Registry) Controller(name string) (plugin.Controller, bool)
- func (r *Registry) Controllers() []plugin.Controller
- func (r *Registry) RegisterController(c plugin.Controller)
- func (r *Registry) RegisterScanner(s plugin.Scanner)
- func (r *Registry) Scanner(name string) (plugin.Scanner, bool)
- func (r *Registry) Scanners() []plugin.Scanner
- type RemoteResolver
- type Result
- type Scope
- type Stats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Waivable ¶ added in v0.52.0
Waivable reports whether a failure under this control name is one --allow-scan-errors can accept.
The flag means "a scanner could not run and I accept a partial result" — the reader has other controls that did run and is choosing to proceed on those. A planning failure is not a scanner: it is the run saying there was nothing to do at all, so there is no partial result to accept. Treating the two alike turns the flag into "pass anyway", and a PASS that means "we did not look" is the worst thing this tool can print.
(sbom) stays waivable on purpose. A missing SBOM is missing evidence, not a missing check — the controls still ran and their verdict still means something.
Types ¶
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine plans and runs scans against a registry of controllers and scanners.
func New ¶
New creates an Engine over the given registry. By default it runs up to NumCPU jobs concurrently.
func (*Engine) Plan ¶
func (e *Engine) Plan(model saga.Model) ([]PlannedJob, error)
Plan expands the model into scan jobs. Only registered controllers that are enabled (project-level for project-scoped controllers, per-component for component-scoped ones) are planned. Controllers are visited in name order for determinism.
type Option ¶
type Option func(*Engine)
Option configures an Engine.
func WithAllowedEffects ¶ added in v0.45.0
WithAllowedEffects accepts scanner effects for this invocation, on top of whatever the Saga accepts. Backs the --allow-effects flag.
func WithCache ¶
WithCache enables result caching: a cache hit for a job's key reuses the stored report instead of re-scanning. A nil cache disables caching (the default).
func WithCacheableTarget ¶ added in v0.61.0
WithCacheableTarget restricts caching to targets the predicate accepts.
Draugr's cache is content-addressed, which holds only while a target's identity is its content. A container image named by a mutable tag breaks that: the name is stable while the bytes behind it are not. This is the hook for a caller that would rather re-scan than be wrong about one.
Nil accepts everything, which is the default.
func WithConcurrency ¶
WithConcurrency sets the maximum number of scan jobs running at once. Values < 1 are ignored (the default is used).
func WithPrioritization ¶ added in v0.5.0
func WithPrioritization(p Prioritizer) Option
WithPrioritization stamps each finding with a priority band computed by p. Priority is applied per run (never cached), since it depends on the component's current classification.
func WithRemoteResolver ¶ added in v0.70.0
func WithRemoteResolver(r RemoteResolver) Option
WithRemoteResolver names local checkouts by the repository they came from.
func WithSBOM ¶ added in v0.41.0
WithSBOM supplies the generator used when a Saga enables config.sbom. Injected rather than imported so pkg/engine stays free of a concrete tool, exactly as it does for scanners. Nil (the default) means a Saga asking for SBOMs gets an error rather than silence.
func WithScope ¶ added in v0.71.0
WithScope narrows the run to named components and controls. The zero Scope scans everything.
func WithWorkingTree ¶ added in v0.64.0
func WithWorkingTree() Option
WithWorkingTree scans repositories as they are on disk, uncommitted work included, instead of at their committed revision.
Also refuses to cache what it scans. A working tree's content changes between two runs at the same revision, so a content-addressed cache keyed on the revision would serve the previous edit's findings — which is the exact opposite of what somebody iterating on a fix needs.
func WithoutPrewarm ¶ added in v0.56.0
func WithoutPrewarm() Option
WithoutPrewarm skips the pre-run warm-up of shared scanner state.
For a run that must make no network calls. The scan still happens against whatever each tool already has on disk, and a tool with nothing on disk reports that itself — which is a more specific message than the engine could produce on its behalf.
type PlannedJob ¶
type PlannedJob struct {
Control string
Job plugin.ScanJob
// Component names the part of the application being scanned, empty for a project-scoped
// control. Carried alongside the classification rather than derived from it: exposure and
// criticality say what a component is worth, and a reader also has to know which one it was.
Component string
Exposure saga.Exposure
Criticality saga.Criticality
}
PlannedJob is a scan job tagged with the control that produced it and the risk classification of the component it targets (empty for project-scoped controls).
type Prioritizer ¶ added in v0.5.0
type Prioritizer func(control string, exposure saga.Exposure, criticality saga.Criticality, res sarif.Result) Priority
Prioritizer computes a finding's priority band from its control and its component's risk classification. Injected via WithPrioritization so the engine stays decoupled from the prioritization matrices and per-control severity floors; nil disables priority stamping.
type Priority ¶ added in v0.56.0
type Priority struct {
// Band is the action band, P1–P4. Empty leaves the finding unstamped.
Band string
// Escalation is set when exploitability data raised the severity the band was computed
// from. Nil when the scanner's own rating stood.
Escalation *sarif.Escalation
}
Priority is what a Prioritizer decided, and why where there is a why.
A struct rather than a bare band because the band alone states a conclusion and withholds its premise — and because the next thing worth explaining about a ranking will not be the last.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the controllers and scanners available to the engine, keyed by name.
func (*Registry) Controller ¶
func (r *Registry) Controller(name string) (plugin.Controller, bool)
Controller returns the named controller, if registered.
func (*Registry) Controllers ¶ added in v0.18.0
func (r *Registry) Controllers() []plugin.Controller
Controllers returns all registered controllers, sorted by Info().Name for stable output.
func (*Registry) RegisterController ¶
func (r *Registry) RegisterController(c plugin.Controller)
RegisterController adds a controller, keyed by its Info().Name.
func (*Registry) RegisterScanner ¶
RegisterScanner adds a scanner, keyed by its Info().Name.
type RemoteResolver ¶ added in v0.70.0
RemoteResolver reports the repository a local checkout was cloned from, or "" when the path is not a local checkout, has no remote, or should not be resolved.
Injected because resolving one means running git, which lives in internal/ — the same arrangement as the SBOM generator. It also makes "do not resolve" expressible by simply not supplying one, which is what a vendored copy or an air-gapped mirror wants: there the path is the more truthful answer, because the remote is absent or names something the tree no longer matches.
type Result ¶
type Result struct {
Controls map[string]plugin.ControlResult
Stats Stats
// Scope is what the run was narrowed to, empty when it was not narrowed at all.
//
// Carried on the result rather than left with the caller because every artifact a result
// becomes has to be able to say so. A scoped report.json shaped exactly like an unscoped one
// is a partial answer that anything downstream will read as a complete one.
Scope Scope
// Suppressed counts findings a config.exclude rule matched. They are still present in the
// reports, marked with their justification — this is how many stopped counting.
Suppressed int
// LapsedExclusions are exclusions past their expiry date, which no longer suppress anything.
// Reported so a finding that used to be accepted does not simply reappear with nothing to
// say why.
LapsedExclusions []saga.ExcludeRule
// UnmatchedExclusions are rules that matched no finding in this run. Reported because an
// exclusion doing nothing is indistinguishable from one that is working: it is usually a
// typo, a rule id that moved, or a finding someone already fixed and forgot to stop excusing.
UnmatchedExclusions []saga.ExcludeRule
// Effects records what this run did to its targets beyond reading them, deduplicated. Only
// scans that actually executed count: a cache hit means the traffic was not sent this time,
// and a record of effects has to describe what happened rather than what was configured.
Effects []plugin.Effect
// Scanners names every scanner this run used, deduplicated and sorted.
//
// Recorded because a report has to be able to say which tools produced its findings — the
// SARIF driver name is the tool's own and does not identify the scanner Draugr selected, so
// nothing downstream could work it out. Cache hits count: the key includes the tool version,
// so a hit describes the same build as the run that stored it.
Scanners []string
// SBOMs are the Software Bills of Materials produced when the Saga enables config.sbom.
// Evidence rather than judgement: they carry no findings and never affect the verdict.
SBOMs []sbom.Document
// ScanErrors records, per control, what stopped it completing — a missing scanner binary, a
// tool that exited badly, a plan that couldn't be built. A control listed here checked less
// than it was asked to, so its absence of findings is not evidence of absence, and callers
// that treat an empty report as "clean" would be wrong.
ScanErrors map[string][]string
}
Result is the outcome of a run: one aggregated ControlResult per control, plus run statistics.
type Scope ¶ added in v0.71.0
type Scope struct {
Components []string
Controls []string
// SkippedComponents are the declared components this scope leaves out, filled in by Resolve.
//
// Carried rather than recomputed because the descriptor is not available everywhere the
// scope is read — a rendered report knows what ran, not what was declared. Naming them
// rather than counting them: "10 not scanned" tells a reader they are missing something and
// not which thing, and the answer is one the run already had.
SkippedComponents []string
}
Scope narrows a run to named components and controls, without changing the descriptor.
The distinction it exists for: `config.controllers` records a decision — this project does not need `dast` — and a filter is a view over one run. Editing the first to get the second is how a temporary change gets committed, and how a control ends up disabled in main because somebody was debugging.
The zero value scans everything, so a caller that never sets one is unaffected. An empty list means "no restriction on this axis", not "nothing": `Scope{Components: []string{"app"}}` runs every control against one component.
A scoped run is still gated and still produces a verdict — the alternative is answering "is my fix good?" with "no verdict", which sends the reader back to a full scan and makes the filter useless for the loop it exists for. What a scoped run must never do is look like an unscoped one, so the scope travels with the result and into every artifact that result becomes.
func (Scope) Empty ¶ added in v0.71.0
Empty reports whether this scope restricts nothing, which is the ordinary case.
func (Scope) IncludesComponent ¶ added in v0.71.0
IncludesComponent reports whether a component is in scope.
Exported because the report has the same question to answer: a component the scope left out must be rendered as not scanned rather than as passing, and only this type knows which those are.
func (Scope) Resolve ¶ added in v0.71.0
Resolve returns a copy of this scope with SkippedComponents filled in from the descriptor, so everything downstream can say what was left out without needing the descriptor again.
func (Scope) Validate ¶ added in v0.71.0
Validate rejects a scope naming something the descriptor or the registry does not have.
A misspelling is the whole failure this guards: `--components frontnd` matches nothing, scans nothing, and passes — the same "we did not look" verdict a filter is otherwise careful not to produce, reached by typo. The error lists what is available, because the reader is one character away from the answer and should not have to go and find it.
type Stats ¶
type Stats struct {
Jobs int
Scans int
CacheHits int
// Deduped counts jobs that reused an identical scan already running/completed in this run
// (in-run singleflight), rather than scanning or hitting the persistent cache.
Deduped int
// Concurrency is the maximum number of scan jobs run in parallel for this run (the
// effective value after applying WithConcurrency or the NumCPU default).
Concurrency int
// Duration is wall-clock for the whole run, and ByControl is how long each control's jobs
// took summed across them. Reported because "why is this slow" is a question a job count
// cannot answer: with concurrency the parts do not add up to the whole, and the control
// worth attention is the slowest one rather than the one with the most jobs.
Duration time.Duration
ByControl map[string]time.Duration
}
Stats summarizes execution, including cache effectiveness.