Documentation
¶
Overview ¶
Package vex reads OpenVEX documents somebody else wrote.
Draugr has produced VEX since it could suppress a finding (`pkg/report`, `--report vex`). This is the other direction: a supplier ships a component and a document saying which of its CVEs do not affect it, and until now the only way to act on that was to retype their analysis into `config.exclude` as though you had decided it. That loses the answer to the question the whole suppression model exists to answer. Who decided this was acceptable, and when.
Reading is a separate package from writing on purpose. The writer builds a document out of a run and owes the reader determinism; this reads a document a stranger produced and owes the run suspicion. Sharing a struct between them would make every field both a promise and an assumption.
OpenVEX only, to start. It is what Draugr already emits, what Trivy and Grype already read, and the smallest of the three formats. So a document can be tested against a real consumer rather than only against a schema. CSAF and CycloneDX VEX carry the same statements in more envelope.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Statuses = []string{ saga.VEXNotAffected, saga.VEXAffected, saga.VEXFixed, saga.VEXUnderInvestigation, }
Statuses are the four OpenVEX statuses. Unlike saga.VEXStatuses, which lists what one of *our* exclusions may declare. Every one of these is readable, including under_investigation, because a supplier saying "we are still looking" is a real and useful thing to be told.
Functions ¶
func Suppresses ¶
Suppresses reports whether a status excuses a finding.
Only not_affected and fixed do. `affected` and `under_investigation` concede exposure rather than excusing it, a supplier telling you that you *are* affected must never be the reason a finding stops being counted, which is what treating every statement alike would do.
Types ¶
type Claim ¶
type Claim struct {
// Vulnerability is the identifier the statement is about, e.g. "CVE-2024-1234".
Vulnerability string
// PURL is the package the claim applies to, the subcomponent when the statement named one,
// otherwise the product itself. Empty when the statement identified neither by package URL, in
// which case the claim applies to every finding for that vulnerability in the component that
// declared the source. See Index.
PURL string
// Status is the claim: not_affected, affected, fixed, under_investigation.
Status string
// Justification is VEX's vocabulary term, present only with not_affected.
Justification string
// Statement is the prose the supplier gave, impact_statement for not_affected, otherwise
// action_statement or status_notes. What a reader needs when deciding whether to believe it.
Statement string
// Author is who asserted it, carried from the document so a claim never travels anonymously.
Author string
// Source is the document this came from, for the same reason.
Source string
// Timestamp is when the claim was made, statement-level where the statement carried one and
// the document's otherwise.
Timestamp string
}
Claim is one supplier statement, resolved to the form a run can act on.
Flattened out of the document's product/subcomponent nesting because that nesting describes how the supplier thinks about their artifact, and a scan finds packages. One statement naming three subcomponents is three claims here, which is what makes matching a lookup rather than a search.
type Document ¶
type Document struct {
// Context is the spec IRI the document claims, e.g. https://openvex.dev/ns/v0.2.0.
Context string `json:"@context"`
// ID identifies this document. Reported so a reader can tell two revisions apart.
ID string `json:"@id"`
// Author is who is making these statements. The single most important field here: a VEX
// statement's weight is entirely a function of who signed it, and a document that does not
// say is one a reader has to be told about rather than quietly obeyed.
Author string `json:"author"`
// Timestamp is when the document was made. A supplier's claim is made at a time they chose,
// not when you scanned, so the report shows its age rather than presenting it as fresh.
Timestamp string `json:"timestamp"`
// Version increments as a document is revised.
Version int `json:"version"`
// Statements are the claims themselves.
Statements []Statement `json:"statements"`
}
Document is an OpenVEX document as read from disk.
Only the fields Draugr acts on or attributes are modeled. An unknown field is ignored rather than rejected: this is somebody else's document, and refusing to read one because it carries a field a later spec added would make Draugr the reason a supplier's claim went unheard.
func Read ¶
Read parses an OpenVEX document.
What it refuses is a document that cannot be acted on: unparseable JSON, or one whose statements carry no status. Everything else is read and reported, because the alternative to a weak document is not a strong one. It is no supplier analysis at all, and the report is where its weakness should be visible.
func (Document) Age ¶
Age is how old the document's timestamp is, and whether it could be read at all.
Reported rather than enforced. How stale is too stale depends on the supplier and the component, and a tool that silently stopped honoring a claim on a date of its own choosing would be making that decision for the operator without telling them.
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is the claims from one or more documents, ready to be asked about a finding.
Built per component. A supplier's claim about their artifact must not reach another component's findings. That is the failure that turns one vendor's assurance into a silent suppression somewhere nobody was looking.
func (*Index) Lookup ¶
Lookup finds the claim covering a finding, if there is one.
A claim naming the package beats one that named no package: the specific statement is the one the supplier thought about.
The key it matched on comes back with it, and has to, because the two maps are keyed differently. A caller that reconstructed the key from the finding would record the wrong one every time a lookup fell through to a package-less claim, and Unmatched would then report a claim that had in fact been applied, a supplier told their correct statement was ignored.
func (*Index) Unmatched ¶
Unmatched names the claims nothing in the run matched, sorted.
A supplier document that matches nothing is doing nothing, and looks exactly like one that is working, the same argument the descriptor's own exclusions are held to. Usually it means the supplier and the scanner disagree about how a package is named, which is a real finding about the document rather than a quiet no-op.
type Product ¶
type Product struct {
ID string `json:"@id"`
Subcomponents []Subcomponent `json:"subcomponents,omitempty"`
}
Product is a thing a statement is about, identified by an IRI, conventionally a package URL.
Subcomponents are the half that does the work here. A supplier says "our product is not affected by CVE-X, which is in libfoo", and libfoo is what a scan of their image actually found. Matching only on the product would miss every such statement, which is most of them.
type Provenance ¶
type Provenance struct {
// Kind is how the document was reached: "path", "url" or "repository".
Kind string
// Location is the path, URL, or `repository#path` as the descriptor named it.
Location string
// Revision is the commit a repository source actually read. Empty for the others.
//
// Resolved even when the descriptor named a branch, which is the point: a ref that moves
// makes a run unreproducible unless the commit it resolved to is written down.
Revision string
// Digest is the sha256 of the bytes read, so two runs can be compared without the document.
Digest string
// ReadAt is when Draugr read it, distinct from the document's own timestamp, and the pair is
// what tells a reader whether they are looking at a fresh copy of a stale claim.
ReadAt time.Time
// Author is the document's author, carried up so the report can attribute without reparsing.
Author string
// Timestamp is the document's own, as written.
Timestamp string
// Statements is how many claims the document carried.
Statements int
// Applied is how many of them matched a finding. Zero against a non-zero Statements is worth
// seeing: a document that excused nothing is indistinguishable from one that was never read,
// and usually means the supplier and the scanner name packages differently.
Applied int
}
Provenance is where a document came from and what it was when it was read.
Recorded because a supplier's VEX is a claim made at a time they chose, not a scan Draugr performed. Every other input to a run can be re-read to reproduce a verdict; this one cannot, because the URL may serve something else tomorrow and the branch may have moved. So the report carries enough to say what was actually applied, where it came from, what it hashed to, and how old the claim was, and a reader who disagrees can check rather than take it on trust.
type Resolved ¶
type Resolved struct {
Document Document
Provenance Provenance
}
Resolved is a document together with where it came from.
type Set ¶
Set is every supplier document a run has, sorted by what it applies to.
Project holds documents that apply to every component; ByComponent holds those a component declared for itself. Kept apart rather than flattened at load, because "who declared this" survives into the report, a claim applied project-wide and one a component asked for are the same statement with different blast radius, and an operator reviewing a suppression wants to know which.
func (Set) Documents ¶
Documents returns every resolved document once, for reporting provenance.
Components in name order rather than map order. Go randomizes map iteration, so without this the provenance block, and the report.json that carries it. Would come out differently on two runs of an unchanged descriptor, which is the opposite of what evidence is for.
func (Set) For ¶
For returns the documents that apply to one component, the component's own first.
Its own first because NewIndex keeps the first of two claims that concede equal exposure, so this is what decides attribution when a project-wide document and a component's own say the same thing: the credit goes to the one somebody chose for this component. Where the two genuinely disagree the stronger claim still wins, whatever the order.
type Statement ¶
type Statement struct {
Vulnerability Vulnerability `json:"vulnerability"`
// Products are what the statement is about, the supplier's own artifact.
Products []Product `json:"products"`
// Status is the claim: not_affected, affected, fixed or under_investigation.
Status string `json:"status"`
// Justification is the machine-readable why, from VEX's fixed vocabulary, valid only with
// not_affected.
Justification string `json:"justification,omitempty"`
// ImpactStatement is the prose why, which VEX accepts in place of a justification.
ImpactStatement string `json:"impact_statement,omitempty"`
// ActionStatement is what is being done about an affected product.
ActionStatement string `json:"action_statement,omitempty"`
// StatusNotes is free text the producer attached to the status.
StatusNotes string `json:"status_notes,omitempty"`
// Timestamp overrides the document's, when a single statement was revised on its own.
Timestamp string `json:"timestamp,omitempty"`
}
Statement is one claim: this vulnerability, against these products, has this status.
type Subcomponent ¶
type Subcomponent struct {
ID string `json:"@id"`
}
Subcomponent is a component inside a product, identified the same way.
type Vulnerability ¶
type Vulnerability struct {
Name string `json:"name"`
}
Vulnerability names the flaw a statement is about.
OpenVEX 0.2.0 made this an object with a `name`; 0.1.0 had a bare string. Both are accepted, because a document written against the older spec is not wrong, it is old. And a supplier who has not revised their tooling is exactly the supplier whose claims are hardest to get hold of.
func (*Vulnerability) UnmarshalJSON ¶
func (v *Vulnerability) UnmarshalJSON(data []byte) error
UnmarshalJSON accepts both the object form and the bare string of OpenVEX 0.1.0.