bom

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: 17 Imported by: 0

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

View Source
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.

View Source
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

func Detect(data []byte) Detection

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

func CompareDocuments(from, to *Document) *Diff

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 Load

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

Load reads and parses an SBOM from a file path.

func LoadBytes

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

LoadBytes parses an SBOM from bytes, unwrapping any attestation envelope.

func LoadReader

func LoadReader(r io.Reader, path string) (*Document, error)

LoadReader reads and parses an SBOM from a stream. path is used only for labelling and may be empty.

func (*Document) Name

func (d *Document) Name() string

Name returns the best available human label for the document's subject.

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.

func (*ErrNoRoot) Error

func (e *ErrNoRoot) Error() string

type FailedDocument

type FailedDocument struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
}

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 Format

type Format string

Format identifies an SBOM serialisation.

const (
	FormatCycloneDX Format = "cyclonedx"
	FormatSPDX      Format = "spdx"
	FormatUnknown   Format = "unknown"
)

type Index

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

Index is a queryable view over a set of documents.

func NewIndex

func NewIndex(docs []*Document) *Index

NewIndex builds an index over the collected documents.

func (*Index) Documents

func (idx *Index) Documents() []*Document

Documents returns the indexed 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

func (idx *Index) Skew() []SkewEntry

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

type LicenseHit struct {
	License    string `json:"license"`
	Components int    `json:"components"`
}

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

type SkewVersion struct {
	Version   string   `json:"version"`
	Documents []string `json:"documents"`
}

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.

func BuildTree

func BuildTree(doc *Document, opts TreeOptions) (*TreeNode, error)

BuildTree constructs a dependency tree from a parsed document.

func (*TreeNode) Count

func (n *TreeNode) Count() int

Count returns the number of nodes in the tree, cycles counted once.

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

type UnsupportedError struct {
	Format      Format
	SpecVersion string
}

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.

type VulnerabilityHit

type VulnerabilityHit struct {
	ID          string   `json:"id"`
	Severity    string   `json:"severity,omitempty"`
	Description string   `json:"description,omitempty"`
	Documents   []string `json:"documents"`
}

VulnerabilityHit is a vulnerability matching a search.

Jump to

Keyboard shortcuts

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