Documentation
¶
Index ¶
- Constants
- Variables
- func Catalogue() (map[string]Preset, error)
- func Exists(name string) bool
- func FindingIDs(findings []Finding) []string
- func FixVersion(findings []Finding, cve string) string
- func GroupByPhase() map[string][]string
- func IsCustomDeclaration(overrides map[string]any) bool
- func List() []string
- func ListByPhase(phase string) []string
- func NewerTag(currentTag string, available []string) string
- func SupersedingVariant(currentTag string, available []string) string
- type ExceptionVerdict
- type Finding
- type Option
- type OptionTOML
- type Preset
- type PresetTOML
- type PresetsFile
- type PromotionDecision
- type ScanDecision
- type Triage
Constants ¶
const ( // ExceptionLive covers a repository the catalogue runs today. Nothing to // decide. ExceptionLive = "live" // ExceptionCarryOver covers a repository the catalogue has left behind, for // a CVE another catalogue repository still carries. Purging it would lose // the justification and block the next scan; it has to be re-filed instead. ExceptionCarryOver = "carry-over" // ExceptionObsolete covers a repository the catalogue has left behind, for a // CVE no catalogue image carries any more. It waives nothing and can go. ExceptionObsolete = "obsolete" // ExceptionUnknown covers a repository the catalogue has left behind, with // no scan evidence to say whether the CVE survived the move. Fail-closed: // not purgeable, because a purge on no evidence is a guess. ExceptionUnknown = "unknown" )
The four states an exception can be in. They are the vocabulary the report and the docs share, so they are spelled once, here.
const ( // ExemptGoStdlib is the Go standard library compiled into a CLI binary. It // vanishes when the publisher recompiles — no action exists at our end — and // the paths flagged (`net/http`, `crypto/*`) are unreachable in a tool that // opens no listener. ExemptGoStdlib = "go-stdlib" // ExemptKernelHeaders is the kernel headers package. The kernel is the // host's and is not in the container; the scanner flags the headers because // they carry a version string. ExemptKernelHeaders = "kernel-headers" )
The two classes the policy exempts by construction, named once here because the reports, the tests and the documentation share the vocabulary.
const PromotionCooldown = 14 * 24 * time.Hour
PromotionCooldown is how long a newly published version must have been publicly available before the catalogue promotes it. The monitor runs weekly, so this is roughly two cycles.
It is deliberately not a user-facing setting: the policy governs cidx's own catalogue, not the projects that use cidx, which pin whatever they want in their own cidx.toml (guardrail 5). Shortening the window is a decision to argue for in a commit, not to bury in a config file.
Variables ¶
var GlobalRegistry map[string]Preset
GlobalRegistry contains all built-in presets Loaded from presets.yaml (dev) or embedded data (production)
Functions ¶
func Catalogue ¶ added in v2.4.0
Catalogue returns the built-in preset catalogue alone: what presets.toml ships, without the user's (~/.config/cidx) or the project's (.cidx/) own presets merged on top.
The image supply-chain policy governs these and only these. A preset declared in a project's .cidx/presets.toml is the project's business (guardrail 1), and letting one reach `cidx preset scan-targets` made the monitor count it as a candidate and run a promotion `sed` for an image presets.toml has never heard of (#248).
It reads the file rather than the registry init() built, which is why it can fail: GlobalRegistry has already swallowed the merge by then.
func FindingIDs ¶ added in v2.4.0
FindingIDs reduces findings to the identifiers the scan gate compares, which works on identity alone.
func FixVersion ¶ added in v2.4.0
FixVersion returns the fix reported for cve among findings, or "".
Used to answer "is this one of the ones an exception must never be written for?" about an identifier rather than about a scan result.
func GroupByPhase ¶
GroupByPhase returns presets grouped by phase
func IsCustomDeclaration ¶
IsCustomDeclaration reports whether the given override section declares a brand-new container (image field present) rather than overriding an existing preset. Used by validator and runner to decide between preset-override semantics and custom-container semantics.
func ListByPhase ¶
ListByPhase returns presets filtered by phase
func NewerTag ¶ added in v2.3.0
NewerTag returns the newest tag in available that is a strictly newer version of currentTag, or "" when nothing on offer qualifies.
Qualifying means the same shape as the tag the catalogue already pins:
Same variant family — same `v` prefix, same suffix after the version. `dhi.io/golang:1.23-alpine3.21-dev` must never be offered a plain `1.24`: that is a different image on a different base, and promoting it would silently change what every Go preset runs on. The same guard keeps `-dev`, `-fips`, `-cli` and `-alpine` variants in their own lane.
Same precision. A registry publishing `0.71` and `0.71.2` offers the first to an image pinned `0.68` and the second to one pinned `0.68.1`. The number of components is a choice the catalogue made about how closely it tracks upstream, and an update is not the place to revisit it.
A currentTag carrying no version (`latest`) qualifies nothing: there is no way to tell what would be newer, and claiming an update would be a guess.
func SupersedingVariant ¶ added in v2.4.0
SupersedingVariant returns the variant family that replaced the one currentTag pins, or "" when nothing has (issue #252).
A variant line does not always die by 404. `dhi.io/golang:1.23-alpine3.21-dev` still pulls, but DHI publishes no `-alpine3.21-dev` tag at all any more — it moved to `-alpine3.24-dev`. No successor will ever appear inside the pinned family, so NewerTag correctly offers nothing, and reporting that as up to date leaves the catalogue on a line upstream abandoned: the quieter cousin of the images deleted outright in #244.
The line counts as frozen only when the repository lists no tag whatsoever in the pinned family. A family still published, even sitting at its own head, is alive and still receives fixes — saying otherwise would fire every week on every image that is merely current.
The successor is found by reading the version the suffix itself carries: `-alpine3.21-dev` is version 3.21 of the `-alpine…-dev` line, so `-alpine3.24-dev` supersedes it while `-alpine3.24-fips-dev` is a different line altogether.
Types ¶
type ExceptionVerdict ¶ added in v2.4.0
type ExceptionVerdict struct {
// State is one of the four constants above.
State string
// Reason states why, for every state — an entry the report leaves in place
// has to say what it is waiting for, or the file goes quiet again.
Reason string
// StillOn names the catalogue repository that still carries the CVE, which
// is the repository the entry has to be re-filed against. Set for carry-over
// alone, so a verdict never implies a repository it did not find the finding
// on.
StillOn string
// FixedIn carries the fix the scanners reported for this CVE, when there is
// one. An exception must never be written for a vulnerability that is fixed
// upstream — that is image freshness, not a decision — so an entry that
// turns out to have a fix is named as such rather than silently renewed.
FixedIn string
}
ExceptionVerdict is what becomes of one exception, in the words the report prints verbatim.
func ClassifyException ¶ added in v2.4.0
func ClassifyException(cve, repository string, running []string, findings map[string][]Finding) ExceptionVerdict
ClassifyException decides what becomes of the exception recorded for cve against repository.
running is the set of repositories the catalogue runs today — registry and path, no tag, no digest. findings maps those same repositories to the HIGH/CRITICAL results the scanners reported on them; a repository absent from the map has an image that was not scanned.
The criterion is the CVE, not the tag. An exception whose repository the catalogue left behind is only obsolete once the findings show no catalogue image carries its CVE any more — and only when every catalogue image has been scanned, since a CVE cannot be shown absent from an image nobody looked at. Anything less is reported as unknown rather than purged, the same fail-closed posture the cooldown and the scan gate take.
The repository test comes first and does not consult the findings, which is load-bearing rather than an optimisation: the security audit generates its ignore file from these very exceptions, so a CVE accepted on a repository the catalogue runs is filtered out of that repository's own scan results by construction. Reading its absence as "gone" would delete every exception that is doing its job, and the next audit would go red on all of them.
An entry still keyed the old way — a whole `repo:tag` where a repository belongs — matches no repository, so it is judged on its CVE alone. That is exactly what re-keying it requires, and it needs no special case.
type Finding ¶ added in v2.4.0
type Finding struct {
// ID is the identifier the scanner reported: a CVE, or a GHSA where the
// scanner knows the advisory but not the CVE it maps to.
ID string
// Severity is HIGH or CRITICAL; the callers filter before constructing one.
Severity string
// Package names the installed package the finding is against, as the
// scanner spells it.
Package string
// PackageType is the ecosystem the package comes from — `gobinary` /
// `go-module` for a Go binary's embedded module list, `debian` / `deb` /
// `apk` / `rpm` for an OS package.
PackageType string
// FixedIn is the version that fixes it, empty when no fix exists at any
// version. Trivy calls it `FixedVersion`, Grype `fix.versions`.
FixedIn string
// EPSS is the probability of exploitation in the next 30 days, 0 when the
// scanner reported none. Grype carries it, Trivy does not. It is reported,
// never thresholded: it tells a human where to look first.
EPSS float64
// KEV records that CISA lists the vulnerability as actively exploited.
// Measured zero on this catalogue, which is exactly why the field is
// reported rather than acted on automatically.
KEV bool
}
Finding is one HIGH/CRITICAL result, reduced to the fields the triage reads.
Both scanners are parsed into this shape: they disagree on spelling (`gobinary` vs `go-module`, `debian` vs `deb`) and on coverage — only Trivy reports the kernel headers package, only Grype reports EPSS — so the triage has to see them side by side rather than one at a time.
func (Finding) Exempt ¶ added in v2.4.0
Exempt names the class that makes this finding unreachable here, or "" when none does.
The two tests are deliberately narrow, because an exemption that is too broad hides findings that matter:
- Go stdlib: the package must be named exactly `stdlib` *and* come from a Go binary. Every other Go module stays in the queue — the runc and containerd findings on the Ansible image are `gobinary` too, and they are the real ones.
- Kernel headers: the package name must be exactly `linux-libc-dev` or `linux-headers`. No type test, because those names belong to no other ecosystem.
type Option ¶
type Option struct {
Type string `yaml:"type" toml:"type"` // string, bool, int, array
Default any `yaml:"default" toml:"default"` // Default value
Description string `yaml:"description" toml:"description"` // Help text
EnvVar string `yaml:"env_var" toml:"env_var"` // Maps to environment variable
CommandFlag string `yaml:"command_flag" toml:"command_flag"` // Maps to command flag
}
Option defines a configurable parameter for a preset
type OptionTOML ¶
type OptionTOML struct {
Type string `toml:"type"`
Default interface{} `toml:"default"`
Description string `toml:"description"`
CommandFlag string `toml:"command_flag"`
EnvVar string `toml:"env_var"`
}
OptionTOML represents an option in TOML format
type Preset ¶
type Preset struct {
Name string `yaml:"name" toml:"name"`
Phase string `yaml:"phase" toml:"phase"`
Image string `yaml:"image" toml:"image"`
Description string `yaml:"description,omitempty" toml:"description,omitempty"` // Usage notes and constraints, shown by `preset info`
Hardened bool `yaml:"hardened,omitempty" toml:"hardened,omitempty"` // Uses Docker Hardened Image (dhi.io)
Command string `yaml:"command" toml:"command"`
Entrypoint []string `yaml:"entrypoint" toml:"entrypoint"`
Workdir string `yaml:"workdir" toml:"workdir"`
Volumes []string `yaml:"volumes" toml:"volumes"`
Env map[string]string `yaml:"env" toml:"env"`
ConfigFiles []string `yaml:"config_files" toml:"config_files"`
Options map[string]Option `yaml:"options" toml:"options"`
RequireCI bool `yaml:"require_ci" toml:"require_ci"` // Requires CI environment
LocalBehavior string `yaml:"local_behavior" toml:"local_behavior"` // draft, no-push, dry-run, disabled
Privileged bool `yaml:"privileged,omitempty" toml:"privileged,omitempty"` // Requires root privileges (skip user mapping)
PullPolicy string `yaml:"pull_policy,omitempty" toml:"pull_policy,omitempty"` // always, if-not-present, never (default: env-based)
Timeout string `yaml:"timeout,omitempty" toml:"timeout,omitempty"` // duration string (e.g., "5m", "45m"), default: 30m
}
Preset defines a complete tool configuration with sensible defaults
func PresetFromOverrides ¶
PresetFromOverrides constructs a Preset from a custom container declaration in cidx.toml. A declaration is a `[containers.NAME]` section that has an `image` field present — that signals a brand-new container, not an override of a known preset. The returned Preset is filled from the overrides map; fields absent from the map keep their zero value.
This implements the user-facing contract documented in examples/cidx-complete.toml (custom containers section) and closes #142.
type PresetTOML ¶
type PresetTOML struct {
Name string `toml:"name"`
Phase string `toml:"phase"`
Image string `toml:"image"`
Description string `toml:"description"`
Hardened bool `toml:"hardened"`
Command string `toml:"command"`
Entrypoint []string `toml:"entrypoint"`
Workdir string `toml:"workdir"`
Volumes []string `toml:"volumes"`
Env map[string]string `toml:"env"`
ConfigFiles []string `toml:"config_files"`
Options map[string]OptionTOML `toml:"options"`
RequireCI bool `toml:"require_ci"`
LocalBehavior string `toml:"local_behavior"`
Privileged bool `toml:"privileged"`
PullPolicy string `toml:"pull_policy"`
Timeout string `toml:"timeout"`
}
PresetTOML represents a preset in TOML format
type PresetsFile ¶
type PresetsFile struct {
Presets map[string]PresetTOML `toml:"presets"`
}
PresetsFile represents the structure of presets.toml
type PromotionDecision ¶ added in v2.2.0
type PromotionDecision struct {
// Promote reports whether the candidate may replace the running image.
Promote bool
// Reason states why. Always set, for promotions as much as for holds — a
// candidate held for another week has to say so somewhere, or the policy
// silently swallows it.
Reason string
// WaivedFor names the vulnerabilities that bought the candidate its way
// past the cooldown. Empty when no waiver was needed, so a promotion never
// claims a waiver that did nothing.
WaivedFor []string
// AgeDays is how long the candidate has been public, in whole days. Nil
// when the registry gave no date.
AgeDays *int
}
PromotionDecision is the verdict on one candidate version, in the words the workflow summary and the promotion PR print verbatim.
func EvaluatePromotion ¶ added in v2.2.0
func EvaluatePromotion(published, now time.Time, affectingUs []string) PromotionDecision
EvaluatePromotion applies the cooldown and its exception to one candidate.
published is when the candidate became publicly available; the zero time means the registry would not say. affectingUs are the HIGH/CRITICAL vulnerabilities already recorded against the image the catalogue runs today.
An undatable candidate is held. That mirrors rule 1's treatment of an unresolvable digest: the promotion is skipped rather than taken on an assumption, and the reason is reported so it does not vanish quietly.
The exception then overrides the hold — for a young candidate and an undatable one alike. Waiting out a date that will never arrive would just leave a known vulnerability in place, and deliberately running a known-vulnerable image to guard against a hypothetical one is the worse trade. A candidate that has served the cooldown claims no waiver: it did not need one.
type ScanDecision ¶ added in v2.3.0
type ScanDecision struct {
// Promote reports whether the findings leave the candidate promotable.
Promote bool
// Reason states why — for a pass as much as for a hold. A candidate the
// scan gate holds has to say so somewhere, or the promotion silently
// swallows it, which is the failure mode #247 was about in the first place.
Reason string
// Introduces names the findings that blocked the promotion: on the
// candidate, on neither the running image's record nor the candidate's own,
// and therefore new. Empty on a pass, so a promotion never implies findings
// it does not have.
Introduces []string
}
ScanDecision is the verdict on what the monitor's scanners found on one candidate, in the words the workflow summary and the promotion PR print verbatim.
func EvaluateScan ¶ added in v2.3.0
func EvaluateScan(found, accepted []string) ScanDecision
EvaluateScan decides whether what the scanners found on a candidate blocks its promotion.
found are the HIGH/CRITICAL vulnerabilities the monitor's scanners reported against the candidate. accepted are the ones already on record for the image the catalogue runs today and for the candidate's own reference (known-vulnerabilities.toml, the file the security audit maintains).
The verdict is differential on purpose. Several catalogue images are knowingly vulnerable — that is exactly what known-vulnerabilities.toml records — so "the candidate has findings" would hold every one of them for ever, and a gate that never passes is worth as little as the one that never failed (#247). What blocks a promotion is a finding that is *new*: reported on the candidate, not already accepted on what we run today.
It follows that a candidate carrying the same vulnerabilities as the running image is promotable. It is not a regression, and refusing it would strand the catalogue on an older image over a finding the candidate merely inherited — while the update it carries goes unapplied.
Comparison is case-insensitive: Trivy spells severities and identifiers in upper case, Grype does not, and the same CVE reported by both must count once.
type Triage ¶ added in v2.4.0
type Triage struct {
// Carried is every distinct HIGH/CRITICAL finding the scanners reported.
// It is the number the baseline was missing: publishing only Accepted is
// how the file came to read "0 accepted findings" on a catalogue carrying
// 596.
Carried int
// Fixable is fixed upstream: image freshness, never exception territory.
Fixable int
// GoStdlib and KernelHeaders are the two exempt classes.
GoStdlib int
KernelHeaders int
// Actionable is what is left: no fix at any version, and reachable enough
// to be worth an argument. The only population an exception is the right
// instrument for.
Actionable int
// KEV names the findings CISA lists as actively exploited, and TopEPSS is
// the highest exploitation probability seen. Both are reported for a human
// to read; neither gates anything.
KEV []string
TopEPSS float64
}
Triage is what a set of findings splits into. The four counts partition the set: Fixable + GoStdlib + KernelHeaders + Actionable == Carried.
func Summarise ¶ added in v2.4.0
Summarise splits the findings on one image into the four populations.
It is per image on purpose: the same CVE on five images is five things to look at, and collapsing them would understate what the catalogue carries. Call it once per image and Add the results.
The class exemptions are labelled before fixability, which inverts the order the policy asks its questions in, and deliberately. The policy orders the questions by what to *do* — a fix exists, so wait, and stop asking. The split here labels findings by *why they are out of the queue*, and the class is the stabler answer: a Go stdlib finding is unreachable whether or not the Go team has shipped the fix yet, and most of them have one, so labelling by fix first would report zero stdlib findings on a catalogue full of them. Actionable is the same number either way, which is the number that matters.
Findings are grouped by identifier first, because both scanners report the same vulnerability and it must be counted once. Within a group:
- exempt only if *every* instance is exempt under the same class — Trivy reporting a CVE against `linux-libc-dev` while Grype reports the same CVE against `openssl` is a real finding, and the narrower reading is the safe one;
- fixable if *any* scanner found a fix — one of them knowing about it is enough for it to exist.