Documentation
¶
Overview ¶
Package store persists scan results and SBOM inventory so the platform can answer questions that a single stateless scan cannot: "which of the images we have ever scanned contain component X at version Y?" (the blast-radius query a team runs the morning a zero-day drops), "is our critical count trending up?", and "who owns the image with the most findings?".
The store is deliberately optional. The CLI and CI paths stay stateless; a server enables the store explicitly. It changes nothing about scan *results* — it only records the Reports the engine already produced and the SBOM inventory that already exists, then indexes them for query.
Two backends share one query core: an in-memory index (NewMemory, used for tests and ephemeral runs) and a flat-file JSON backend (Open, one file per scan, atomic writes, rebuilt into the index on open). No SQL, no cgo, no external driver — a design choice, not a limitation (see NOTES.md).
Index ¶
- type API
- type Component
- type ComponentMatch
- type ComponentQuery
- type FindingHit
- type FindingQuery
- type ImageSummary
- type Scan
- type Store
- func (s *Store) Dir() string
- func (s *Store) Get(id string) (*Scan, bool)
- func (s *Store) Inventory() []ImageSummary
- func (s *Store) Len() int
- func (s *Store) Put(sc *Scan) (string, error)
- func (s *Store) QueryComponent(q ComponentQuery) []ComponentMatch
- func (s *Store) QueryFindings(q FindingQuery) []FindingHit
- func (s *Store) Scans() []*Scan
- func (s *Store) Trends(image string) []TrendPoint
- type TrendPoint
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API serves store queries and (optionally) triggers scans that it persists.
type Component ¶
type Component struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Type string `json:"type,omitempty"`
PURL string `json:"purl,omitempty"`
}
Component is one inventoried package, flattened out of an SBOM for indexing. We keep only what blast-radius queries need; the full SBOM lives in the scan's Report metadata or can be regenerated. Name/Version are lower-cased on ingest so matching is case-insensitive without re-normalizing on every query.
func ComponentsFromSBOM ¶
ComponentsFromSBOM flattens an SBOM into the store's lightweight component records. A nil SBOM yields nil, so callers can pass the result of a best-effort SBOM build directly (a Dockerfile target has no SBOM, and that is fine).
type ComponentMatch ¶
type ComponentMatch struct {
Image string `json:"image"`
Digest string `json:"digest,omitempty"`
Version string `json:"version"`
PURL string `json:"purl,omitempty"`
ScanID string `json:"scan_id"`
RecordedAt time.Time `json:"recorded_at"`
Owner string `json:"owner,omitempty"`
}
ComponentMatch is one image found to contain the queried component.
type ComponentQuery ¶
type ComponentQuery struct {
Name string // component name (case-insensitive); empty matches any
Version string // exact version; empty matches any version
PURL string // exact purl; empty ignored
// LatestPerImage keeps only the most recent scan per image, so a component
// removed in a newer scan does not keep flagging the image.
LatestPerImage bool
}
ComponentQuery selects scans by an inventoried component. This is the zero-day workflow: "advisory just dropped for openssl < 3.0.7 — where is it?".
type FindingHit ¶
type FindingHit struct {
Image string `json:"image"`
ScanID string `json:"scan_id"`
RecordedAt time.Time `json:"recorded_at"`
Owner string `json:"owner,omitempty"`
Finding engine.Finding `json:"finding"`
}
FindingHit is a finding plus the scan context needed to act on it.
type FindingQuery ¶
type FindingQuery struct {
Image string
Module string
RuleID string
Owner string
MinSeverity engine.Severity
Since time.Time
Until time.Time
}
FindingQuery filters findings across the whole store. Zero-valued fields are ignored, so an empty query returns everything.
type ImageSummary ¶
type ImageSummary struct {
Image string `json:"image"`
Digest string `json:"digest,omitempty"`
Owner string `json:"owner,omitempty"`
LastScanned time.Time `json:"last_scanned"`
Scans int `json:"scans"`
Components int `json:"components"`
Counts map[string]int `json:"counts"` // severity name → count (latest scan)
Total int `json:"total"`
}
ImageSummary is one row of the inventory view: an image, its latest scan's severity posture, and how much we know about it.
type Scan ¶
type Scan struct {
// ID is a deterministic content hash (see computeID). Re-storing the same
// image at the same recorded time overwrites rather than duplicates.
ID string `json:"id"`
Image string `json:"image"` // canonical identity for grouping (image ref or path)
Digest string `json:"digest,omitempty"`
TargetType string `json:"target_type,omitempty"`
RecordedAt time.Time `json:"recorded_at"`
Labels map[string]string `json:"labels,omitempty"` // ownership: owner, team, env, …
Report *engine.Report `json:"report"`
Components []Component `json:"components,omitempty"`
}
Scan is one persisted analysis run: the engine Report, the artifact's component inventory, and the identity/ownership needed to group and attribute it. It is a plain value so it JSON-marshals directly to the file backend.
func BuildScan ¶
func BuildScan(image string, rep *engine.Report, comps []Component, labels map[string]string, fallback time.Time) *Scan
BuildScan assembles a Scan from a finished Report plus optional inventory and ownership labels. RecordedAt is taken from the Report so the persisted record carries the same (injected, deterministic) timestamp the scan ran with; if the report has no timestamp the caller-supplied fallback is used.
image is the canonical identity used for grouping and trends. When empty it falls back to the report's target, so a caller can always persist something addressable.
func RunAndBuild ¶
func RunAndBuild(ctx context.Context, eng *engine.Engine, target *engine.Target, names []string, image string, labels map[string]string, withSBOM bool, now time.Time) *Scan
RunAndBuild runs the engine against target and (best-effort, when withSBOM is set) builds its component inventory, returning a Scan ready to Put. It does not persist — the caller decides whether to store it. An empty names slice runs every module. This is the single scan-and-assemble path shared by the HTTP API and the MCP scan_target tool, so both produce identically-shaped records.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a queryable, thread-safe inventory of scans. Construct it with NewMemory (ephemeral) or Open (file-backed). All query methods return deterministically ordered results.
func NewMemory ¶
func NewMemory() *Store
NewMemory returns an ephemeral, in-memory store. Nothing is persisted; ideal for tests and for server runs that do not want a data directory.
func Open ¶
Open returns a file-backed store rooted at dir, creating the directory if needed and loading any scans already present. Corrupt or oversized files are skipped with a returned error listing them, but valid records still load — one bad file never blocks the whole inventory.
func (*Store) Inventory ¶
func (s *Store) Inventory() []ImageSummary
Inventory returns one summary per distinct image, using each image's most recent scan for the severity posture. Sorted by descending critical+high count then image name, so the riskiest images sort to the top.
func (*Store) Put ¶
Put records (or replaces) a scan and returns its id. The scan is copied defensively so later caller mutations cannot corrupt the index.
func (*Store) QueryComponent ¶
func (s *Store) QueryComponent(q ComponentQuery) []ComponentMatch
QueryComponent answers the blast-radius question: which stored images contain the queried component (optionally pinned to a version). Results are sorted by image then version then recorded time for a stable, human-scannable order.
func (*Store) QueryFindings ¶
func (s *Store) QueryFindings(q FindingQuery) []FindingHit
QueryFindings returns findings matching the filter, most-severe first, then by image and rule id. Attribution (image, owner, scan) travels with each hit so a caller can route it without a second lookup.
func (*Store) Scans ¶
Scans returns every stored scan, newest first (ties broken by id for stability).
func (*Store) Trends ¶
func (s *Store) Trends(image string) []TrendPoint
Trends returns the time-ordered posture for an image (oldest first) so a UI can plot whether findings are climbing or being burned down. An empty image aggregates every scan chronologically.