analyze

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 32 Imported by: 0

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

View Source
const (
	StatusNotPresent   = ecosystem.StatusNotPresent
	StatusNotInPath    = ecosystem.StatusNotInPath
	StatusLinked       = ecosystem.StatusLinked
	StatusReachable    = ecosystem.StatusReachable
	StatusUndetermined = ecosystem.StatusUndetermined
)
View Source
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

func Validate(opts Options) error

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 Corrections added in v0.8.0

type Corrections struct {
	Count int `json:"count"`
	// Advisories are the ids set aside, sorted, so the list is stable between
	// runs and diffable.
	Advisories []string `json:"advisories"`
	// Details spell out, per advisory, the ranges that excluded the version.
	Details []string `json:"details"`
}

Corrections is what the advisory database offered and the scan did not report, because the record's own precise version ranges exclude the version it was matched against. internal/osv/customranges.go has the mechanism and the conditions.

It exists for the reason Withheld does. Setting an advisory aside is the direction this tool must never be wrong in, and a scan that quietly returned 27 fewer findings than the database offered would be indistinguishable from a cleaner image. So the count is carried out to the report and named, and a reader who disagrees with the arithmetic can check it: every dropped id is listed and every one of them is still one OSV lookup away.

type Descriptor added in v0.7.0

type Descriptor struct {
	Tool     string    `json:"tool,omitempty"`     // "vexscan"
	Version  string    `json:"version,omitempty"`  // "v0.6.2"
	Started  time.Time `json:"started,omitempty"`  // when the command began
	Duration string    `json:"duration,omitempty"` // "12.4s"

	// AdvisorySource is where the advisories were read from -- today always
	// the live OSV API, and the one field an offline or cached database would
	// change. It is recorded even when nothing was queried, because "which
	// database said nothing" is the question an empty report raises.
	AdvisorySource string `json:"advisory_source,omitempty"`

	// AdvisoriesAsOf is when that source answered, which for a live API is how
	// fresh the report is. Zero when the scan resolved no advisories at all --
	// itself worth telling apart from a scan that resolved some and found
	// nothing.
	AdvisoriesAsOf time.Time `json:"advisories_as_of,omitempty"`
}

Descriptor records what produced a report.

A report outlives the run that made it. Six months on, the two questions a reader has are which build of the tool wrote it and how stale the advisories behind it are, and neither is recoverable from the findings: an empty report from a scan run this morning and an empty report from a build that predates the CVE are the same bytes.

The fields split by who can honestly answer them. AdvisorySource and AdvisoriesAsOf are set here, because only the resolver knows which database answered and when. Tool, Version, Started and Duration are left to the caller: the timing of a command is the command's fact, and reading a wall clock for it inside this package would make every test of it depend on one.

type Finding

type Finding = ecosystem.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" | "rpm" | "sbom"
	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"`

	// Notes are things the reader of this list has to know that are not
	// omissions: packages that were read but have no column here, and the like.
	// Unreadable is for what could not be read, and conflating the two would
	// make a complete inventory report as incomplete.
	Notes []string `json:"notes,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 ".")

	// RPM are package files to scan without installing them (--rpm): a file, a
	// directory of them, or a URL. Mutually exclusive with the other three.
	//
	// This is the one target with no filesystem behind it, and every
	// difference in the report follows from that: no reachability closure can
	// run, so nothing can be linked and nothing can be ruled out as
	// unreachable. What a header can still answer -- would this package
	// install any code at all -- it does.
	RPM []string

	// RPMDeep is --rpm-deep. It decompresses each package's cpio payload and
	// extracts its ELF objects, so the OS plugin can read their dynamic symbol
	// tables. It does not change what verdicts are reachable in kind -- there
	// is still no filesystem and no entrypoint, so nothing is ever linked --
	// but it lets the dynsym-absent test rule a finding out as not_present when
	// the vulnerable function is exported by nothing the package ships. It only
	// bites alongside --mine-advisories, which is what supplies the symbol to
	// look for. See the extract half of internal/rpmsrc.
	RPMDeep bool

	// SBOM is a CycloneDX JSON bill of materials to scan (--sbom): a file, or
	// "-" for standard input. Mutually exclusive with the other four targets.
	//
	// It is --rpm's weaker sibling. --rpm has no filesystem either, but an rpm
	// header still lists the files the package would install, so it can rule a
	// package out on the grounds that it ships no code. A CycloneDX component
	// is a name, a version and a purl, so nothing here can be ruled out and
	// every finding it produces is undetermined. See ecosystem.SBOMFinding.
	SBOM string

	// 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)
	// contains filtered or unexported fields
}

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"`

	// Corrections are advisories the database matched but its own precise
	// ranges exclude, and is nil when there were none. See its doc comment and
	// internal/osv/customranges.go.
	Corrections *Corrections `json:"corrections,omitempty"`

	// Descriptor records what produced this report. See its doc comment.
	Descriptor *Descriptor `json:"descriptor,omitempty"`
}

Result is the full analysis output.

func Run

func Run(ctx context.Context, opts Options) (*Result, error)

Run dispatches to filesystem analysis -- an image or a rootfs -- or to source-repo analysis.

func (*Result) Failed

func (r *Result) Failed() bool

Failed reports whether the findings are an incomplete account of the target -- because an ecosystem could not complete, or because part of the tree could not be read.

type Status

type Status = ecosystem.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 counts every finding in the catalog, whatever this scan
	// decided about it -- including the ones it ruled out and the ones a vendor
	// already answered. All four counters here have that population, because
	// they describe the lookup and not the verdict.
	//
	// The text report's priority line counts the affected rows instead, since
	// that is the part a reader has to act on, and says so in words when the
	// two differ. They are allowed to differ. They are not allowed to differ
	// silently: see writePriority.
	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.

Jump to

Keyboard shortcuts

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