Documentation
¶
Overview ¶
Package vexpr writes OpenVEX documents recording the findings vexscan ruled out, laid out as a VEX Hub repository so they can be contributed to one.
It is the write counterpart to internal/vex, which only reads. The split is deliberate and matches the invariant that package documents: nothing that reaches a verdict may also publish one. vexpr never touches a finding's status -- it reads the verdict local evidence already produced and serialises the ruled-out ones into the format a hub distributes, so the documents say exactly what the scan said and nothing the scan did not.
It writes to a directory and stops there. Getting those files into a hub is a pull request against somebody else's repository, and that is git's job and gh's job: they already handle forks, signing, branch protection and the review itself, and a hand-rolled API client handles none of them. Splitting there also puts a human in front of the diff, which for a statement that tells other people's scanners to stop reporting a vulnerability is the point rather than an inconvenience.
Index ¶
Constants ¶
const (
StatusNotAffected = "not_affected"
)
OpenVEX statuses and the context vexscan writes. Only not_affected is emitted here -- a ruled-out finding is the vendor-independent form of that claim -- but the constant set is kept complete so a reader of this file sees the whole vocabulary the format allows.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Doc ¶
type Doc struct {
Context string `json:"@context"`
ID string `json:"@id,omitempty"`
Author string `json:"author"`
// Version is any rather than int because published hubs disagree about it:
// OpenVEX calls for a number and some tooling writes "1" as a string. A
// typed int made the string form a decode failure, and a decode failure in
// this package used to mean the document was replaced wholesale. Nothing
// here reads the value -- a preserved document re-emits it verbatim -- so
// the loosest type that round-trips is the right one.
Version any `json:"version"`
Timestamp string `json:"timestamp"`
Statements []Statement `json:"statements"`
// contains filtered or unexported fields
}
Doc is an OpenVEX document as it is written to a hub.
It is a separate type from vex.Doc, which is the decoded read-side shape: this one carries the on-wire nesting and every field a published document needs, including the @context and @id that the reader throws away.
A Doc parsed from an existing hub file keeps that file's raw top-level members (in their original order) in original/order, and each existing Statement keeps its own raw bytes. Re-marshaling then reproduces every field the format allows -- not just the subset this type models -- so appending to a vendor-authored document never silently strips fields off the statements it already holds. A Doc built fresh (original == nil) is marshaled from the typed shape instead.
func NewDoc ¶
NewDoc starts an empty document for a hub, with the context and author every statement in it will share.
func ParseDoc ¶
ParseDoc decodes a document already published in the hub, so new statements can be merged into it without dropping the ones it already holds.
A missing or malformed document is not an error the caller has to distinguish: ParseDoc returns ok=false in that case, because "the hub has no file here yet" and "start a fresh document" have the same next step.
The parsed document keeps its raw top-level members (and each statement keeps its raw bytes), so a later Marshal reproduces every field OpenVEX allows, not just the ones this type models.
func (*Doc) Marshal ¶
Marshal renders the document as the pretty-printed JSON a hub stores, in the formatting of the file it was parsed from -- two spaces and a trailing newline for a document created here.
func (*Doc) MarshalJSON ¶
MarshalJSON renders the document, preserving the original file's fields and order when it came from an existing hub document and emitting the typed shape otherwise. Only the statements array and the top-level timestamp -- the two things a merge changes -- are overwritten on the preserved form.
type FileChange ¶
FileChange is one file to write, path relative to the output directory (and so also relative to the hub root, since the two share a layout).
type HubReader ¶
type HubReader interface {
// IndexRaw is index.json exactly as published.
IndexRaw() []byte
// Raw returns one file's bytes by a location the index gave, or ok=false
// when the hub has no such file.
Raw(ctx context.Context, loc string) ([]byte, bool, error)
}
HubReader is the read side of a VEX hub: enough to merge into what a hub already publishes without holding a credential that could write to it.
*vex.Hub satisfies it, and that is the point. The hub this merges against is the same hub --vexhub already read during the scan, over the same transport, with the same URL-or-directory handling -- there is no second notion of what a hub is, and no second way to reach one.
type Options ¶
type Options struct {
// Hub is the hub to merge against, read-only. Nil starts from an empty
// index, which is how a hub gets bootstrapped rather than added to.
Hub HubReader
// Author is the OpenVEX author recorded on every statement written. It has
// no default: an author is a claim of responsibility for the assertion, and
// there is nobody but the caller who can make it.
Author string
// Timestamp is the scan time, used on every statement so a re-run of the
// same scan produces the same document.
Timestamp string
// Logf receives progress lines. Nil discards them.
Logf func(string, ...any)
}
Options configures a proposal.
type Plan ¶
type Plan struct {
Changes []FileChange
Products []ProductChange
Statements int
// Skipped is how many ruled-out findings could not be written as a
// matchable statement (no product, component or id).
Skipped int
// Unparsable is every hub document that exists but could not be decoded,
// and was therefore left exactly as the hub published it. Reported rather
// than counted silently: each one is a product this proposal says nothing
// about, and a reader would otherwise have no way to tell that from a
// product with nothing to say.
Unparsable []string
}
Plan is every file a proposal would write, computed but not yet on disk.
Computing and writing are separate so the caller can report what is about to happen, and so the whole merge is testable without a filesystem.
func Propose ¶
Propose computes the documents that record this scan's ruled-out findings, merged into whatever the hub already publishes. It writes nothing; Write does.
func (*Plan) Empty ¶
Empty reports whether the proposal would change nothing -- every ruled-out finding was already covered, or there were none to begin with.
func (*Plan) Write ¶
Write puts the plan on disk under dir, creating parent directories as needed.
dir may be a clone of the hub itself, which is the intended shape: merge against the clone, write back into it, and read the result as a git diff. Nothing is written outside dir -- the paths were vetted where they were built, and vetted again here, because this is the step that touches a filesystem and the check that matters is the one nearest the syscall.
type Product ¶
type Product struct {
ID string `json:"@id"`
Subcomponents []Subcomponent `json:"subcomponents,omitempty"`
}
Product is one artifact a statement covers and, optionally, the components inside it the vulnerability actually belongs to.
type ProductChange ¶
ProductChange records, for the summary, which vulnerabilities were added to one product's document.
type ProductProposal ¶
type ProductProposal struct {
// Product is the artifact purl the statements are filed under.
Product string
// Statements are the not_affected claims, one per ruled-out finding, sorted
// so a repeated run produces a byte-identical document.
Statements []Statement
}
ProductProposal is every statement proposed for one product's document.
type Statement ¶
type Statement struct {
Vulnerability Vulnerability `json:"vulnerability"`
Products []Product `json:"products"`
Status string `json:"status"`
Justification string `json:"justification,omitempty"`
// ImpactStatement is the author's own sentence explaining the conclusion.
// For a vexscan-authored statement it is how the tool reached the verdict,
// which is the single most useful field for a human reviewing the PR.
ImpactStatement string `json:"impact_statement,omitempty"`
ActionStatement string `json:"action_statement,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
// contains filtered or unexported fields
}
Statement is one claim about one vulnerability in one product.
A Statement decoded from an existing document keeps its raw bytes so that re-marshaling reproduces it verbatim, preserving OpenVEX fields (status_notes, @id, version, supplier, ...) that this type does not model. A Statement built in-process has raw == nil and is marshaled from its typed fields.
func (Statement) MarshalJSON ¶
MarshalJSON emits the original bytes for a statement read from an existing document, and the typed shape for one built in-process.
func (*Statement) UnmarshalJSON ¶
UnmarshalJSON decodes the fields this type models while keeping the raw bytes, so an existing statement round-trips without losing fields Statement omits.
type Subcomponent ¶
type Subcomponent struct {
ID string `json:"@id"`
}
Subcomponent is one dependency inside a product a statement is scoped to.
type Vulnerability ¶
type Vulnerability struct {
Name string `json:"name"`
ID string `json:"@id,omitempty"`
Aliases []string `json:"aliases,omitempty"`
}
Vulnerability is the id a statement is filed under plus every alias it is also known by, so a later lookup keyed on any of them still finds it.