analyze

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 26 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 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"
	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.

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