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
// 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"`
}
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 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.