Documentation
¶
Overview ¶
Package bom is the read side of the SBOM story: it parses SBOM documents the CLI did not write.
internal/cdx builds CycloneDX documents from a scanned working tree, and internal/license writes SPDX. Neither can consume one. Everything that follows from treating an SBOM as an *input* — diffing two releases, applying a third-party VEX to a supplied BOM, querying a directory of documents for version skew — needs a parser first, and this is it.
One canonical model: everything parsed becomes a *cdx.BOM. SPDX in, CycloneDX model out. That keeps a single type flowing into license evaluation, VDB lookup, VEX application and diff, rather than each of them growing a second code path for the other serialisation. What the original document was is not lost — it is recorded on Document.Source and stamped into the BOM's metadata properties, so a normalised document still says where it came from.
Index ¶
- Constants
- type BlastRadius
- type ChangeKind
- type CollectOptions
- type Collected
- type ComponentChange
- type ComponentHit
- type Detection
- type Diff
- type DiffSummary
- type Document
- type DocumentSummary
- type Entry
- type Envelope
- type ErrNoRoot
- type FailedDocument
- type FieldReport
- type Format
- type Index
- type LicenseHit
- type Location
- type QualityReport
- type SearchResults
- type SkewEntry
- type SkewVersion
- type SourceInfo
- type TreeNode
- type TreeOptions
- type UnsupportedError
- type VulnChange
- type VulnerabilityHit
Constants ¶
const ( PropSourceFormat = "vulnetix:bom/source-format" PropSourceSpec = "vulnetix:bom/source-spec-version" PropSourceEnvelope = "vulnetix:bom/source-envelope" PropSourceDigest = "vulnetix:bom/source-digest" PropSourcePath = "vulnetix:bom/source-path" PropPredicateType = "vulnetix:bom/predicate-type" )
Property names stamped onto a parsed document's metadata.
Normalising SPDX into the CycloneDX model is lossy by construction. These properties are what makes it honest: a document that has been through this reader always says what it originally was, at which spec version, inside which envelope, and — via the digest — exactly which bytes it came from. `bom enrich` depends on the digest to assert fidelity, and `bom diff` uses it to notice it is being handed the same document twice.
const MinSearchQuery = 2
MinSearchQuery is the shortest query accepted.
One character matches most of a corpus, which is not a search result — it is the corpus with extra steps.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BlastRadius ¶
type BlastRadius struct {
// Query is what was asked for.
Query string `json:"query"`
// Key is the package identity that matched.
Key string `json:"key,omitempty"`
// Name is the component's name as the documents spell it.
Name string `json:"name,omitempty"`
// Locations are every document containing it.
Locations []Location `json:"locations"`
// Versions are the distinct versions present, ordered.
Versions []string `json:"versions"`
// DirectCount and TransitiveCount split the locations by directness.
// Unknown is where the document had no graph to answer from.
DirectCount int `json:"directCount"`
TransitiveCount int `json:"transitiveCount"`
UnknownCount int `json:"unknownCount"`
}
BlastRadius is the answer to "who has this package".
type ChangeKind ¶
type ChangeKind string
ChangeKind classifies a component-level change.
const ( ChangeAdded ChangeKind = "added" ChangeRemoved ChangeKind = "removed" ChangeUpgraded ChangeKind = "upgraded" ChangeDowngraded ChangeKind = "downgraded" // ChangeVersionChanged is a version that moved in a way semver could not // order — a git sha replacing a tag, a distro epoch, a date stamp. Reported // as movement without a direction rather than guessed at. ChangeVersionChanged ChangeKind = "version-changed" ChangeLicense ChangeKind = "license-changed" )
type CollectOptions ¶
type CollectOptions struct {
// Paths are files, directories or globs to read.
Paths []string
// Recursive walks directories to any depth. Off by default: a directory of
// SBOMs is the normal shape, and recursing into a source tree would sweep
// up every package.json it finds.
Recursive bool
// MaxDepth bounds a recursive walk. Zero means unlimited.
MaxDepth int
}
CollectOptions controls document collection.
type Collected ¶
type Collected struct {
// Documents are the SBOMs that parsed.
Documents []*Document
// Skipped names files that were examined and were not SBOMs.
Skipped []string
// Failed names files that looked like SBOMs and could not be read, with
// the reason. A corpus query over a partly-unreadable set must say so:
// silently answering from fewer documents than the user pointed at is how
// a "no results" answer becomes a wrong answer.
Failed []FailedDocument
}
Collected is the outcome of gathering documents.
func Collect ¶
func Collect(opts CollectOptions) (*Collected, error)
Collect gathers SBOM documents from the given paths.
type ComponentChange ¶
type ComponentChange struct {
Kind ChangeKind `json:"kind"`
// Name and Purl identify the component; Purl is empty when neither side had one.
Name string `json:"name"`
Purl string `json:"purl,omitempty"`
// FromVersion and ToVersion are empty for the side that does not exist.
FromVersion string `json:"fromVersion,omitempty"`
ToVersion string `json:"toVersion,omitempty"`
// FromLicense and ToLicense are rendered licence expressions.
FromLicense string `json:"fromLicense,omitempty"`
ToLicense string `json:"toLicense,omitempty"`
// Direct reports whether the component is a direct dependency of the
// subject on whichever side it exists. Nil when there is no dependency
// graph to answer from — an unmeasurable fact stays null, never false.
Direct *bool `json:"direct,omitempty"`
}
ComponentChange is one component-level difference.
type ComponentHit ¶
type ComponentHit struct {
Key string `json:"key"`
Name string `json:"name"`
Versions []string `json:"versions"`
Documents int `json:"documents"`
}
ComponentHit is a component matching a search.
type Detection ¶
type Detection struct {
Format Format
SpecVersion string
Envelope Envelope
PredicateType string
// Supported reports whether this CLI can parse the document. An
// unsupported document still reports its Format and SpecVersion, so the
// caller can say *which* version it could not read.
Supported bool
// Payload is the inner SBOM bytes. It equals the input when no envelope
// was unwrapped, so callers can parse Payload unconditionally.
Payload []byte
}
Detection is the result of sniffing a document.
func Detect ¶
Detect identifies an SBOM document, unwrapping any attestation envelope.
Envelope unwrapping is the point of this function. A container SBOM produced by Syft or BuildKit is almost never a bare document — it arrives as an in-toto Statement whose `predicate` is the SBOM, often base64'd inside a DSSE envelope first. Sniffing for `bomFormat` at the top level misses all of them.
Detect does not verify signatures. Unwrapping is parsing, not trust; a caller that needs the attestation checked calls internal/attest explicitly (`bom import --verify-attestation`), so that reading a document never implies believing it.
type Diff ¶
type Diff struct {
From SourceInfo `json:"from"`
To SourceInfo `json:"to"`
Identical bool `json:"identical"`
Summary DiffSummary `json:"summary"`
Components []ComponentChange `json:"components,omitempty"`
Vulns []VulnChange `json:"vulnerabilities,omitempty"`
}
Diff is the full comparison of two documents.
func CompareDocuments ¶
CompareDocuments diffs two parsed documents.
type DiffSummary ¶
type DiffSummary struct {
Added int `json:"added"`
Removed int `json:"removed"`
Upgraded int `json:"upgraded"`
Downgraded int `json:"downgraded"`
VersionChanged int `json:"versionChanged"`
LicenseChanged int `json:"licenseChanged"`
VulnsAdded int `json:"vulnsAdded"`
VulnsRemoved int `json:"vulnsRemoved"`
FromComponents int `json:"fromComponents"`
ToComponents int `json:"toComponents"`
GraphEdgesAdded int `json:"graphEdgesAdded"`
GraphEdgesGone int `json:"graphEdgesRemoved"`
}
DiffSummary is the headline count set.
func (DiffSummary) Total ¶
func (s DiffSummary) Total() int
Total is the number of component-level changes.
type Document ¶
type Document struct {
BOM *cdx.BOM `json:"bom"`
Source SourceInfo `json:"source"`
}
Document is a parsed SBOM in the canonical CycloneDX model.
func LoadReader ¶
LoadReader reads and parses an SBOM from a stream. path is used only for labelling and may be empty.
type DocumentSummary ¶
type DocumentSummary struct {
Path string `json:"path"`
Subject string `json:"subject,omitempty"`
Version string `json:"version,omitempty"`
Format Format `json:"format"`
SpecVersion string `json:"specVersion,omitempty"`
Components int `json:"components"`
Vulnerabilities int `json:"vulnerabilities"`
Timestamp string `json:"timestamp,omitempty"`
Deployment cdx.DeploymentContext `json:"deployment,omitzero"`
}
DocumentSummary describes one document in the corpus.
type Entry ¶
type Entry struct {
// Document is the SBOM the component was found in.
Document *Document
// Component is the component itself.
Component *cdx.Component
// Direct reports whether the document's subject depends on it directly.
// Nil when the document has no dependency graph to answer from — an
// unmeasurable fact stays null, never false.
Direct *bool
}
Entry is one component occurrence in one document.
type Envelope ¶
type Envelope string
Envelope identifies an outer wrapper the SBOM arrived inside.
const ( // EnvelopeNone — the document is a bare SBOM. EnvelopeNone Envelope = "" // EnvelopeDSSE — a DSSE envelope: the in-toto Statement is base64 in // `payload`, alongside `signatures`. This is what `cosign attest` and // `syft attest` produce. EnvelopeDSSE Envelope = "dsse" // EnvelopeInToto — a bare in-toto Statement, predicate inline. This is what // BuildKit writes into an image's SBOM attestation layer, and what falls out // of a DSSE envelope once decoded. EnvelopeInToto Envelope = "in-toto" )
type ErrNoRoot ¶
type ErrNoRoot struct{ Requested string }
ErrNoRoot is returned when a tree has no starting point.
type FailedDocument ¶
FailedDocument is a file that looked like an SBOM but could not be parsed.
type FieldReport ¶
type FieldReport struct {
// Field is the stable machine name, e.g. "component.version".
Field string `json:"field"`
// Label is the human description shown in terminal output.
Label string `json:"label"`
// Present counts components (or 1/0 for document-level fields) carrying it.
Present int `json:"present"`
// Total is the denominator for Present.
Total int `json:"total"`
// Detail explains a shortfall, or is empty when the field is complete.
Detail string `json:"detail,omitempty"`
}
FieldReport is the result for one checked field.
func (FieldReport) Complete ¶
func (f FieldReport) Complete() bool
Complete reports whether every subject carried the field.
func (FieldReport) Ratio ¶
func (f FieldReport) Ratio() float64
Ratio is the fraction of subjects carrying the field, 0 when nothing applied.
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is a queryable view over a set of documents.
func (*Index) Search ¶
func (idx *Index) Search(query string, limit int) *SearchResults
Search finds components, documents, vulnerabilities and licences by name.
limit bounds each facet independently, so a query matching a thousand components still shows the one document and the two vulnerabilities it also matched, rather than burying them.
func (*Index) Skew ¶
Skew finds packages present at inconsistent versions across the corpus.
This is the "why do we have fifty containerds" question, and it is the one that most often has an actionable answer: a package at four versions across six services is usually four upgrades nobody sequenced, not four deliberate pins.
func (*Index) Summaries ¶
func (idx *Index) Summaries() []DocumentSummary
Summaries describes every document in the corpus.
func (*Index) Where ¶
func (idx *Index) Where(selector string) *BlastRadius
Where finds every document containing a package.
The selector may be a purl (with or without a version), a bare name, or a substring. Exact identities are tried first so that asking for "lodash" does not silently answer about "lodash.merge" when both are present.
type LicenseHit ¶
LicenseHit is a licence matching a search.
type Location ¶
type Location struct {
Path string `json:"path"`
Subject string `json:"subject,omitempty"`
Version string `json:"version"`
Direct *bool `json:"direct,omitempty"`
Deployment cdx.DeploymentContext `json:"deployment,omitzero"`
}
Location is one document a package was found in.
type QualityReport ¶
type QualityReport struct {
// Document-level fields: author, timestamp, subject, unique identifier.
Document []FieldReport `json:"document"`
// Component-level fields, each scored across all components.
Components []FieldReport `json:"components"`
// ComponentCount is the number of components scored.
ComponentCount int `json:"componentCount"`
// Score is the unweighted mean of every field's ratio, 0–100. It is a
// sorting key, not a grade.
Score int `json:"score"`
}
QualityReport summarises the completeness of an SBOM.
func Quality ¶
func Quality(doc *Document) *QualityReport
Quality scores a document's field completeness.
func (*QualityReport) Incomplete ¶
func (r *QualityReport) Incomplete() []FieldReport
Incomplete returns the fields that are not fully populated, worst first.
type SearchResults ¶
type SearchResults struct {
Query string `json:"query"`
Components []ComponentHit `json:"components,omitempty"`
Documents []DocumentSummary `json:"documents,omitempty"`
Vulnerabilities []VulnerabilityHit `json:"vulnerabilities,omitempty"`
Licenses []LicenseHit `json:"licenses,omitempty"`
// Totals are the full counts per facet, before any limit was applied, so a
// truncated result says how much it truncated.
Totals map[string]int `json:"totals"`
}
SearchResults are the facets a query matched.
type SkewEntry ¶
type SkewEntry struct {
Key string `json:"key"`
Name string `json:"name"`
Versions []SkewVersion `json:"versions"`
// DocumentCount is how many documents carry it at any version.
DocumentCount int `json:"documentCount"`
// DirectCount is how many of those depend on it directly, which is where a
// version can actually be changed.
DirectCount int `json:"directCount"`
}
SkewEntry is one package present at more than one version.
type SkewVersion ¶
SkewVersion is one version of a skewed package and where it appears.
type SourceInfo ¶
type SourceInfo struct {
// Path is where the document was read from. Empty for stdin.
Path string `json:"path,omitempty"`
// Format and SpecVersion are the original serialisation.
Format Format `json:"format"`
SpecVersion string `json:"specVersion,omitempty"`
// Envelope and PredicateType are set when the SBOM arrived inside an
// attestation wrapper.
Envelope Envelope `json:"envelope,omitempty"`
PredicateType string `json:"predicateType,omitempty"`
// Digest is the SHA-256 of the bytes as supplied — the outer envelope
// included, not the unwrapped payload, because that is the artefact a user
// can point at on disk.
Digest string `json:"digest"`
// Size is the length in bytes of those same supplied bytes.
Size int `json:"size"`
}
SourceInfo records what a Document was before normalisation.
type TreeNode ¶
type TreeNode struct {
Ref string `json:"ref"`
Name string `json:"name"`
Version string `json:"version,omitempty"`
Purl string `json:"purl,omitempty"`
Depth int `json:"depth"`
Children []*TreeNode `json:"children,omitempty"`
// Cycle marks a node whose subtree was elided because it repeats an
// ancestor. Dependency graphs do contain cycles (Go modules and Maven both
// permit them); eliding and saying so beats either infinite recursion or
// silently truncating.
Cycle bool `json:"cycle,omitempty"`
// Elided marks a node whose children were cut off by the depth limit.
Elided bool `json:"elided,omitempty"`
}
TreeNode is one node in a rendered dependency tree.
type TreeOptions ¶
type TreeOptions struct {
// Root selects the starting component by purl, bom-ref or name. Empty
// starts from the document's subject.
Root string
// Invert builds the reverse tree: children are the components that depend
// on the parent.
Invert bool
// MaxDepth caps traversal depth. Zero means unlimited.
MaxDepth int
}
TreeOptions controls tree construction.
type UnsupportedError ¶
UnsupportedError reports a document this CLI recognised but cannot parse.
func (*UnsupportedError) Error ¶
func (e *UnsupportedError) Error() string
type VulnChange ¶
type VulnChange struct {
Kind ChangeKind `json:"kind"` // ChangeAdded or ChangeRemoved
ID string `json:"id"`
// Severity is the highest severity rating carried on the entry, or "".
Severity string `json:"severity,omitempty"`
// Affects lists the component refs the entry points at.
Affects []string `json:"affects,omitempty"`
}
VulnChange is one vulnerability-level difference.