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
- Variables
- func Fatal(problems []Problem) bool
- func WriteOpenVEX(statements []Statement, opts WriteOptions) ([]byte, error)
- type AppliedStatement
- type Document
- type Finding
- type Format
- type Match
- type MatchBasis
- type Problem
- type Product
- type Result
- type Set
- type Statement
- type StatementSource
- type Status
- type WriteOptions
Constants ¶
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 ¶
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 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 LoadAll ¶
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 (*Document) Validate ¶
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.
func Detect ¶
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) Suppresses ¶
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 ¶
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 ¶
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 (*Set) Match ¶
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 ¶
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 ¶
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 ¶
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.
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.