store

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 16 Imported by: 0

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

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.

func NewAPI

func NewAPI(s *Store, eng *engine.Engine) *API

NewAPI builds a store API. Pass a non-nil engine to enable POST /v1/scans; pass nil for a read-only inventory API.

func (*API) Register

func (a *API) Register(mux *http.ServeMux)

Register wires every store route onto mux. The master calls this once from server.go's routes() when the store is enabled.

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

func ComponentsFromSBOM(doc *sbomlib.SBOM) []Component

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

func Open(dir string) (*Store, error)

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) Dir

func (s *Store) Dir() string

Dir reports the backing directory, or "" for a memory store.

func (*Store) Get

func (s *Store) Get(id string) (*Scan, bool)

Get returns a scan by id.

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) Len

func (s *Store) Len() int

Len reports how many scans are stored.

func (*Store) Put

func (s *Store) Put(sc *Scan) (string, error)

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

func (s *Store) Scans() []*Scan

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.

type TrendPoint

type TrendPoint struct {
	ScanID     string         `json:"scan_id"`
	RecordedAt time.Time      `json:"recorded_at"`
	Counts     map[string]int `json:"counts"`
	Total      int            `json:"total"`
}

TrendPoint is the severity posture of one image at one point in time.

Jump to

Keyboard shortcuts

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