Documentation
¶
Overview ¶
Package analyze orchestrates the vexscan pipeline: prepare a target (extract an image, open a rootfs, or check out a source tree), ask each ecosystem plugin what it finds, resolve advisories for what the plugins inventory, and optionally overlay an LLM assessment on the genuinely-affected results.
The division of labour is deliberate. Plugins own the *deterministic* question — is this vulnerable code present, and can it run — and nothing else. This package owns advisory resolution and the LLM overlay, so no plugin can make the model's opinion load-bearing.
Index ¶
Constants ¶
const ( StatusNotPresent = ecosystem.StatusNotPresent StatusNotInPath = ecosystem.StatusNotInPath StatusLinked = ecosystem.StatusLinked StatusReachable = ecosystem.StatusReachable StatusUndetermined = ecosystem.StatusUndetermined )
const SchemaVersion = 2
SchemaVersion is the version of the JSON Result shape.
1 was gomod-vex: Go only, one module per run. 2 adds the ecosystem-neutral finding identity, the per-ecosystem outcome list, and OS package findings. Every version-1 field is still present and still means what it meant.
Variables ¶
This section is empty.
Functions ¶
func Validate ¶
Validate reports whether the options describe a coherent scan, touching neither the network nor the disk. It lets the caller tell a bad command line from a failed scan, and report the former before the pull rather than after.
It cannot catch everything: a bare --package name is resolved against the inventory, so whether it names anything is only knowable once the target has been read.
Types ¶
type Finding ¶
The finding vocabulary lives in internal/ecosystem, which is what the plugins produce. These aliases keep the existing analyze.Finding / analyze.Status spelling working for callers and keep the JSON output byte-identical.
type InventoryResult ¶
type InventoryResult struct {
Target string `json:"target"`
Mode string `json:"mode"` // "image" | "rootfs"
OS *OSInfo `json:"os,omitempty"`
Databases []pkgdb.Result `json:"databases"`
// Unreadable is the part of the tree the walks could not enter. An
// inventory that skipped a directory is a list of what is installed with
// an unknown number of omissions, which is not the same document.
Unreadable *target.Unreadable `json:"unreadable,omitempty"`
// Languages are the installed distributions of the language ecosystems
// that ship inside images: Python's site-packages, Node's node_modules.
// They are kept separate from Databases because they overlap: Debian's
// python3-yaml deb installs the same files a PyPI inventory reports under
// "pyyaml", and merging the two would hide that both advisory namespaces
// apply.
Languages []langdb.Result `json:"languages,omitempty"`
}
InventoryResult is what a tree's package databases say is installed.
This is the raw material the OS ecosystem plugin works from, exposed on its own because it is checkable: a user who suspects a finding is wrong can see exactly which database row it came from, and the ecosystem string that will be used to query OSV before any query is made.
func Inventory ¶
func Inventory(ctx context.Context, opts Options) (*InventoryResult, error)
Inventory reads the OS package databases of an image or a rootfs.
It deliberately does not require a subject: "what is in this tree" is a question worth answering on its own, and it is the one output that can be checked against `dpkg -l` or `rpm -qa` run inside the same tree.
func (*InventoryResult) LanguagePackages ¶
func (r *InventoryResult) LanguagePackages() int
LanguagePackages counts the installed language distributions.
It is kept apart from Packages rather than added to it because the two overlap -- the same files can be one deb and one PyPI distribution -- so a single total would be a number that counts some code twice and means nothing.
func (*InventoryResult) Packages ¶
func (r *InventoryResult) Packages() int
Packages counts the OS packages the inventory found.
type OSInfo ¶
type OSInfo struct {
ID string `json:"id,omitempty"`
VersionID string `json:"version_id,omitempty"`
PrettyName string `json:"pretty_name,omitempty"`
// Ecosystem is the OSV ecosystem string, or empty with EcosystemError set.
Ecosystem string `json:"ecosystem,omitempty"`
EcosystemError string `json:"ecosystem_error,omitempty"`
}
OSInfo is the distribution identity read from /etc/os-release.
type Options ¶
type Options struct {
Image string
// RootFS is a filesystem tree already on disk -- an unpacked image, a
// mounted volume, a machine's own /. It runs the image analyzers against a
// tree nobody extracted, so it skips the pull but also arrives without an
// image config: see runTree.
RootFS string
Repo string // git repo (source mode); mutually exclusive with Image
Ref string // branch/tag/commit for Repo
Path string // module subdirectory within Repo (default ".")
// Packages are the raw --package selectors: purls, ecosystem:name
// shorthand, or bare names resolved against whatever inventory contains
// them. See ecosystem.ParseSubject.
Packages []string
// Module is the deprecated --module flag, equivalent to one
// --package golang:MODULE.
Module string
// All requests everything each plugin can inventory, rather than a named
// list of packages.
All bool
// Ecosystems restricts which plugins run (--ecosystem). Empty runs them
// all. Naming one nothing handles is an error, not an empty result.
Ecosystems []string
// Severities restricts the result to findings carrying these severity
// labels (--severity), already canonicalized through cvss.Parse by the
// caller. Empty keeps everything.
//
// Unlike Ecosystems this changes what is reported rather than what runs:
// every plugin still inventories and every advisory is still resolved,
// because a finding's severity is only knowable once its advisory is in
// hand. What it does buy is that the LLM overlay is never asked about a row
// nobody is going to read.
Severities []string
CVEs []string // optional filter; empty means "every advisory that applies"
Version string // optional override of the detected module version (image mode)
OS string
Arch string
// Roots are extra entrypoints for the reachability closures -- the OS
// plugin's shared libraries and the language plugins' import graphs -- for
// an image whose real command comes from outside its config.
Roots []string
// DlopenPolicy decides whether a reachable dlopen blocks conclusions.
DlopenPolicy elfgraph.DlopenPolicy
// DynamicPolicy decides whether a reachable import of a computed name
// blocks conclusions. It is the import graph's DlopenPolicy.
DynamicPolicy modgraph.DynamicPolicy
// OSVEcosystem overrides the OSV ecosystem derived from the image's
// os-release, for the distributions os-release does not determine. It is
// not the same knob as Ecosystems, which chooses which plugins run.
OSVEcosystem string
// VEXHubs are VEX Hub repositories to check findings against (--vexhub),
// in priority order: the first hub with a statement about a finding wins,
// so an internal hub listed ahead of a vendor's overrides it.
//
// A statement never changes a finding's status. It records that someone has
// already published an answer, which the report uses to decide what a
// reader still has to look at.
VEXHubs []string
// Triage is the EPSS/KEV loader for --triage, or nil to skip it entirely.
// It is the loader rather than a bool so a test can point it at its own
// feeds, and so the caller owns the cache location.
//
// Like VEXHubs it never changes a finding's status. Whether a vulnerability
// is being exploited elsewhere says nothing about whether the code is
// present here, which is the only question this tool answers; what it
// changes is which of the answers a reader looks at first.
Triage *triage.Loader
// GoVersion optionally pins the Go toolchain for repo-mode analysis
// (e.g. "1.24.0"). Mainly useful with --module stdlib, whose findings depend
// on the toolchain version.
GoVersion string
UseLLM bool
// LLMEndpoint, LLMModel and LLMCommand say who to ask. Each falls back to
// VEXSCAN_LLM_ENDPOINT / _MODEL / _COMMAND when empty, and exactly one of
// endpoint and command must end up set: see llm.Config. The credential is
// read from the environment only, never from here, so it cannot reach a
// command line.
LLMEndpoint string
LLMModel string
LLMCommand string
// MineAdvisories lets the model read each advisory's prose for symbol,
// soname and filename leads, which plugins then validate against the image.
// Requires UseLLM.
MineAdvisories bool
// TrustImportAbsence lets the OS plugin conclude not_in_execute_path when
// nothing the closure reaches imports the vulnerable symbol. Off by default:
// the vulnerable function is usually called from inside the same library,
// where no dynamic import records it.
TrustImportAbsence bool
// Logf receives progress messages (may be nil).
Logf func(format string, args ...any)
}
Options configure a run. Set exactly one of Image, RootFS or Repo.
type Result ¶
type Result struct {
SchemaVersion int `json:"schema_version"`
Target string `json:"target"` // image ref, rootfs directory, or repo
Mode string `json:"mode"` // "image" | "rootfs" | "repo"
Module string `json:"module"`
Findings []Finding `json:"findings"`
// Ecosystems records how each plugin fared. It exists so a failure is
// never indistinguishable from a clean result: a plugin that found a
// package database and could not read it reports the error here and
// contributes no findings at all.
Ecosystems []ecosystem.EcosystemResult `json:"ecosystems,omitempty"`
// Unreadable is the part of the target tree the scan could not enter,
// accumulated across every plugin that walked it. It is nil in repo mode,
// which analyzes a checkout the current user just created.
//
// It is set for the same reason it is recorded at all: a directory nothing
// looked inside contributes no findings, which is exactly what a directory
// full of nothing wrong contributes. Only one of those is good news.
Unreadable *target.Unreadable `json:"unreadable,omitempty"`
// VEXHubs records what each --vexhub contributed, including one that could
// not be read. It is not part of Failed(): see vexOverlay for why a hub
// failure is not the same kind of incompleteness as an ecosystem failure.
VEXHubs []ecosystem.VEXHubResult `json:"vex_hubs,omitempty"`
// Withheld is what --severity removed from Findings, and is nil when the
// flag was not used or hid nothing. See severityFilter: a filtered result
// and a clean one are indistinguishable without it.
Withheld *Withheld `json:"withheld,omitempty"`
// Triage records what --triage contributed, and is nil when the flag was
// not used. Like VEXHubs it is not part of Failed(): see triageOverlay.
Triage *TriageResult `json:"triage,omitempty"`
}
Result is the full analysis output.
type Status ¶
The finding vocabulary lives in internal/ecosystem, which is what the plugins produce. These aliases keep the existing analyze.Finding / analyze.Status spelling working for callers and keep the JSON output byte-identical.
type TriageResult ¶ added in v0.5.0
type TriageResult struct {
// EPSSDate and KEVDate are the feeds' own dates, read out of the payloads.
// A cached percentile is a claim about a day, and a report read next month
// must not be able to pretend otherwise.
EPSSDate string `json:"epss_date,omitempty"`
KEVDate string `json:"kev_date,omitempty"`
// Stale means the network could not be reached and a previously downloaded
// copy was used.
EPSSStale bool `json:"epss_stale,omitempty"`
KEVStale bool `json:"kev_stale,omitempty"`
EPSSError string `json:"epss_error,omitempty"`
KEVError string `json:"kev_error,omitempty"`
// Scored is how many findings got a percentile. NoCVE is those whose
// advisory carries no CVE id at all, and NotInFeed those that had one the
// feed did not know -- almost always a CVE published in the last day or
// two. They are counted apart because the report has to explain the two
// differently, and because neither of them means "low risk".
Scored int `json:"scored"`
NoCVE int `json:"no_cve,omitempty"`
NotInFeed int `json:"not_in_feed,omitempty"`
KnownExploited int `json:"known_exploited"`
CatalogSize int `json:"catalog_size,omitempty"`
}
TriageResult records what --triage contributed, including what it could not.
It sits beside VEXHubs in Result and, like VEXHubs, is deliberately not part of Failed(). An unreachable EPSS mirror does not make the report claim a clean image it never examined -- it leaves the findings in the order they were already in. That is a different kind of wrong from an ecosystem that could not be inventoried, and only one of them may pass silently.
func (*TriageResult) Unscored ¶ added in v0.5.0
func (t *TriageResult) Unscored() int
Unscored is how many findings have no percentile, for whichever reason.
func (*TriageResult) Usable ¶ added in v0.5.0
func (t *TriageResult) Usable() bool
Usable reports whether either feed produced anything to sort by. When it is false the report keeps its severity ordering and says why.
type Withheld ¶ added in v0.4.0
type Withheld struct {
// Severities is what --severity asked to keep, so the banner can quote the
// flag back rather than making the reader remember what they typed.
Severities []string `json:"severities"`
Count int `json:"count"`
// BySeverity is what was dropped, keyed by label. UNKNOWN in here is the
// entry that matters: those findings are unrated, not unimportant.
BySeverity map[string]int `json:"by_severity"`
}
Withheld records what --severity removed from the result.
It exists because a filtered report and a clean one look identical, and that is the one confusion this tool must never cause. Every renderer prints this before the findings, so a short list is always accompanied by the reason it is short.
It is deliberately not part of Failed(). The scan completed and read everything it meant to; the reader asked for a subset of what it found. That is the opposite of an ecosystem that could not be inventoried, where the tool does not know what it missed.