vex

package
v3.100.1 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package vex is the read side of VEX: consuming statements this CLI did not write.

internal/triage generates OpenVEX and CycloneDX VEX from the CLI's own triage decisions. Nothing could read a VEX document back in — so a statement an upstream vendor published about their own product, saying a CVE does not affect the configuration you actually ship, had no way to reach a scan. That is the case VEX exists for, and it was the one case the CLI could not serve.

One statement model. OpenVEX, CycloneDX VEX and CSAF VEX all express the same assertion — this vulnerability, against this product, has this status, for this reason — in three different shapes. Normalising them here means matching and application are written once instead of three times.

Index

Constants

View Source
const (
	PropVEXSource  = "vulnetix:vex/source"
	PropVEXAuthor  = "vulnetix:vex/author"
	PropVEXBasis   = "vulnetix:vex/match-basis"
	PropVEXExplain = "vulnetix:vex/explain"
	PropVEXDocID   = "vulnetix:vex/document-id"
)

Property names recording VEX provenance on an annotated vulnerability.

Provenance is not decoration. Once a finding is suppressed, the only remaining question a reader has is "who said so, and on what grounds" — and without these the answer is nowhere in the document.

Variables

View Source
var Justifications = map[string]bool{
	"component_not_present":                             true,
	"vulnerable_code_not_present":                       true,
	"vulnerable_code_not_in_execute_path":               true,
	"vulnerable_code_cannot_be_controlled_by_adversary": true,
	"inline_mitigations_already_exist":                  true,
}

Justifications are the OpenVEX impact-analysis justifications.

A not_affected statement without one is invalid per the OpenVEX spec: the whole point of the status is the argument behind it, and an unjustified "this does not affect us" is an assertion nobody can audit.

Functions

func Fatal

func Fatal(problems []Problem) bool

Fatal reports whether any problem makes a statement unusable.

func WriteOpenVEX

func WriteOpenVEX(statements []Statement, opts WriteOptions) ([]byte, error)

WriteOpenVEX serialises statements as an OpenVEX 0.2.0 document.

Deliberately separate from triage.GenerateOpenVEX. That function turns the CLI's own triage findings into statements; this one round-trips statements that already exist. Sharing them would mean one function serving two callers with different inputs and different obligations — the triage writer may invent a justification for a fixed finding, and a merge must never invent anything the source documents did not say.

Types

type AppliedStatement

type AppliedStatement struct {
	VulnID     string     `json:"vulnId"`
	Purl       string     `json:"purl,omitempty"`
	Status     Status     `json:"status"`
	Suppressed bool       `json:"suppressed"`
	Basis      MatchBasis `json:"basis"`
	Explain    string     `json:"explain"`
	Source     string     `json:"source,omitempty"`
}

AppliedStatement records one statement reaching one finding.

type Document

type Document struct {
	Format     Format      `json:"format"`
	ID         string      `json:"id,omitempty"`
	Author     string      `json:"author,omitempty"`
	Path       string      `json:"path,omitempty"`
	Statements []Statement `json:"statements"`
}

Document is a parsed VEX document.

func Load

func Load(path string) (*Document, error)

Load reads and parses a VEX document from a file.

func LoadAll

func LoadAll(paths []string) ([]*Document, []string, error)

LoadAll reads every VEX document under the given paths.

A path may be a file or a directory; directories are walked one level deep for .json files. A file that is not VEX is skipped rather than fatal, because the usual invocation is `--vex .vulnetix/` and that directory also holds SBOMs and SARIF — refusing to run because a sibling file is not VEX would be obstructive. A file that IS VEX but fails to parse is an error, because the user pointed at it deliberately.

func LoadBytes

func LoadBytes(data []byte, path string) (*Document, error)

LoadBytes parses a VEX document from bytes.

func (*Document) Validate

func (d *Document) Validate() []Problem

Validate checks a document for structural and semantic problems.

Structural validity is not enough for VEX: a document can be schema-valid and still assert nothing usable — a not_affected with no justification, a statement naming no vulnerability, a status outside the vocabulary. Those are what a consumer actually trips over, so they are what this reports.

type Finding

type Finding struct {
	// VulnID is the vulnerability identifier as the scanner reports it.
	VulnID string
	// Purl identifies the affected component, when known.
	Purl string
	// Name and Version are the fallback identity for a finding with no purl.
	Name    string
	Version string
}

Finding is the subset of a scan finding that matching needs.

type Format

type Format string

Format identifies a VEX serialisation.

const (
	FormatOpenVEX   Format = "openvex"
	FormatCycloneDX Format = "cyclonedx-vex"
	FormatCSAF      Format = "csaf-vex"
	FormatUnknown   Format = "unknown"
)

func Detect

func Detect(data []byte) Format

Detect identifies the VEX serialisation of a document.

Order matters. A CycloneDX VEX document is a CycloneDX document, so it is checked by bomFormat before anything else; CSAF announces itself with a document.category of csaf_vex; OpenVEX by its @context. A document that answers to none of them is not VEX, and saying so is better than guessing.

type Match

type Match struct {
	Statement Statement  `json:"statement"`
	Basis     MatchBasis `json:"basis"`
	// Explain is a human sentence saying why this statement was applied.
	Explain string `json:"explain"`
}

Match is one statement applied to one finding.

func (Match) Status

func (m Match) Status() Status

Status is the matched statement's assertion.

func (Match) Suppresses

func (m Match) Suppresses() bool

Suppresses reports whether the matched statement closes the finding.

type MatchBasis

type MatchBasis string

MatchBasis explains why a statement was applied.

const (
	// BasisExactPurl — the statement names this exact purl, version included.
	BasisExactPurl MatchBasis = "exact-purl"
	// BasisPurlAnyVersion — the statement names the package with no version
	// scope, so it speaks about every version of it.
	BasisPurlAnyVersion MatchBasis = "purl-any-version"
	// BasisPurlVersionListed — the statement lists this version explicitly.
	BasisPurlVersionListed MatchBasis = "purl-version-listed"
	// BasisPurlVersionRange — the finding's version falls inside a range the
	// statement scopes itself to.
	BasisPurlVersionRange MatchBasis = "purl-version-range"
	// BasisSubcomponent — the statement narrows to this purl as a
	// subcomponent of the product it names.
	BasisSubcomponent MatchBasis = "subcomponent"
	// BasisNameVersion — matched on name and version because neither side
	// carried a purl.
	BasisNameVersion MatchBasis = "name-version"
	// BasisDocumentWide — the statement names no product, so it speaks about
	// everything the document describes. The weakest basis, and the one most
	// likely to over-apply, so it loses to every other.
	BasisDocumentWide MatchBasis = "document-wide"
)

type Problem

type Problem struct {
	// StatementIndex is the position of the offending statement, or -1 for a
	// document-level problem.
	StatementIndex int `json:"statementIndex"`
	// Message describes what is wrong.
	Message string `json:"message"`
	// Fatal marks a problem that makes the statement unusable, as opposed to
	// one that only makes it weaker.
	Fatal bool `json:"fatal"`
}

Problem is a validation finding against a document.

type Product

type Product struct {
	// Purl is the product's package URL, when it has one. This is the field
	// matching actually uses; ID is a fallback for documents that identify
	// products some other way.
	Purl string `json:"purl,omitempty"`
	// ID is the raw product identifier as written in the document.
	ID string `json:"id,omitempty"`
	// Versions are explicit versions the statement is scoped to. Empty means
	// the statement applies to every version of the product.
	Versions []string `json:"versions,omitempty"`
	// Subcomponents are purls the statement narrows to within the product.
	Subcomponents []string `json:"subcomponents,omitempty"`
}

Product identifies what a statement is about.

type Result

type Result struct {
	// Total is every vulnerability entry considered.
	Total int `json:"total"`
	// Effective is the count still live after VEX.
	Effective int `json:"effective"`
	// Suppressed is the count a not_affected or fixed statement closed.
	Suppressed int `json:"suppressed"`
	// Annotated is the count that gained an analysis block without being
	// suppressed — an `affected` statement adds an action statement, which is
	// useful, but does not close anything.
	Annotated int `json:"annotated"`
	// Applied lists each finding a statement reached, for reporting.
	Applied []AppliedStatement `json:"applied,omitempty"`
	// Unmatched is the number of statements that matched no finding. A high
	// count usually means the documents are about a different product, which
	// is worth telling the user rather than silently doing nothing.
	Unmatched int `json:"unmatchedStatements"`
}

Result reports what applying a statement set did.

func Apply

func Apply(bom *cdx.BOM, set *Set) *Result

Apply folds a statement set into a BOM's vulnerability entries.

The BOM is modified in place: each matched entry gains an analysis block and provenance properties. Nothing is removed.

func (*Result) SuppressedIDs

func (r *Result) SuppressedIDs() []string

SuppressedIDs returns the vulnerability ids VEX closed.

type Set

type Set struct {
	// contains filtered or unexported fields
}

Set is a queryable collection of statements.

func NewSet

func NewSet(docs []*Document) *Set

NewSet indexes statements for matching.

func (*Set) Empty

func (s *Set) Empty() bool

Empty reports whether the set has no statements.

func (*Set) Len

func (s *Set) Len() int

Len is the number of indexed statements.

func (*Set) Match

func (s *Set) Match(f Finding) (Match, bool)

Match finds the statement that best speaks about a finding.

When several statements match, the most specific basis wins; on equal specificity the newest timestamp wins, because VEX is a running assertion and a later statement supersedes an earlier one about the same thing.

func (*Set) Statements

func (s *Set) Statements() []Statement

Statements returns every indexed statement.

type Statement

type Statement struct {
	// VulnID is the vulnerability identifier, normalised to a bare id.
	VulnID string `json:"vulnId"`
	// Aliases are other identifiers for the same vulnerability.
	Aliases []string `json:"aliases,omitempty"`
	// Status is the assertion.
	Status Status `json:"status"`
	// Justification explains a not_affected status.
	Justification string `json:"justification,omitempty"`
	// ImpactStatement is free text supporting a not_affected status.
	ImpactStatement string `json:"impactStatement,omitempty"`
	// ActionStatement is what the consumer should do about an affected status.
	ActionStatement string `json:"actionStatement,omitempty"`
	// Products are the things this statement is about. Empty means the
	// statement applies to the document's whole subject.
	Products []Product `json:"products,omitempty"`
	// Timestamp is when the assertion was made. Used to pick a winner when two
	// statements cover the same (vulnerability, product).
	Timestamp time.Time `json:"timestamp,omitempty"`
	// Source describes where the statement came from, for attribution in
	// output — a suppressed finding must always be traceable to the document
	// that suppressed it.
	Source StatementSource `json:"source"`
}

Statement is one normalised VEX assertion.

func Merge

func Merge(docs []*Document) []Statement

Merge combines documents, newest statement per (vulnerability, product) wins.

A later statement supersedes an earlier one about the same thing — that is what VEX is for. A vendor who published "under_investigation" in March and "not_affected" in June has said the second thing, and a merge that kept both would leave the consumer to guess.

type StatementSource

type StatementSource struct {
	// Path is the file the statement was read from.
	Path string `json:"path,omitempty"`
	// Format is the serialisation it was written in.
	Format Format `json:"format"`
	// DocumentID is the document's own identifier.
	DocumentID string `json:"documentId,omitempty"`
	// Author is who published it.
	Author string `json:"author,omitempty"`
}

StatementSource attributes a statement to its document.

type Status

type Status string

Status is a VEX status label.

The four OpenVEX statuses are the common denominator: CycloneDX's analysis states and CSAF's product-status categories both map onto them without loss for the purpose that matters here, which is deciding whether a finding is still live.

const (
	// StatusNotAffected — the product is not affected. Requires a justification.
	StatusNotAffected Status = "not_affected"
	// StatusAffected — the product is affected and no remediation is applied.
	StatusAffected Status = "affected"
	// StatusFixed — the vulnerability has been remediated.
	StatusFixed Status = "fixed"
	// StatusUnderInvestigation — the status is not yet known.
	StatusUnderInvestigation Status = "under_investigation"
)

func (Status) Suppresses

func (s Status) Suppresses() bool

Suppresses reports whether a status means the finding is no longer live.

not_affected and fixed are assertions that the finding does not apply; affected and under_investigation are not. A suppressed finding is still reported and badged — see the total/effective/suppressed split — never silently dropped.

func (Status) Valid

func (s Status) Valid() bool

Valid reports whether the status is one this CLI understands.

type WriteOptions

type WriteOptions struct {
	// ID is the document identifier. Generated when empty.
	ID string
	// Author is the document author.
	Author string
	// Tooling identifies what produced the document.
	Tooling string
	// Now overrides the timestamp, for deterministic tests.
	Now time.Time
}

WriteOptions controls OpenVEX serialisation.

Jump to

Keyboard shortcuts

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