Documentation
¶
Overview ¶
Package sbom is the SBOM codec: it projects a dependency graph into an SPDX 2.3 or CycloneDX document and reads such a document back into a graph.
It owns the document model (Document, Component, Dependency and the assertions they carry), the two format codecs, the strict preflight that refuses a document without one unambiguous reading (bomly-cli ADR-0039), and the projection in both directions: FromDepGraph and FromGraphEntries build a Document from a graph, ToGraph builds a graph from a Document, and MarshalJSON / UnmarshalJSON move a Document to and from bytes.
This package is the one home for that behaviour (bomly-cli ADR-0045). It was the CLI's internal/sbom, copied into two plugin repositories and drifted there; the plugins and then the CLI adopt this package from the release that carries it and delete their copies. What a document may name of a graph -- the questions every renderer and exporter asks of a node -- is the sibling package graphview.
Index ¶
- Constants
- Variables
- func ComponentEcosystem(component Component) model.Ecosystem
- func DocumentAssertionsFor(doc *Document) *model.DocumentAssertions
- func IsProjectRootComponent(c Component) bool
- func MarshalDepGraphJSON(g *model.Graph, target Target, buildOpts BuildOptions, ...) ([]byte, error)
- func MarshalGraphEntriesJSON(g *model.Graph, entries []model.GraphEntry, target Target, ...) ([]byte, error)
- func MarshalJSON(doc *Document, target Target, opts EncodeOptions) ([]byte, error)
- func ToGraph(doc *Document) (*model.Graph, error)
- func ToGraphEntry(doc *Document, manifest model.ManifestMetadata) (model.GraphEntry, error)
- func UnmarshalAutoJSON(data []byte) (*Document, Target, error)
- type BuildOptions
- type Component
- type ComponentOrigin
- type Dependency
- type Digest
- type Document
- type EOL
- type EncodeOptions
- type License
- type ProjectRoot
- type Provenance
- type Target
- type Vulnerability
Constants ¶
const MaxDocumentBytes = 256 << 20
MaxDocumentBytes bounds what any ingest entry point will read. The strict preflight and the codecs do work proportional to the input, and the CLI bounds a file at exactly this size before it reads it; a consumer of this package handing over bytes it obtained some other way gets the same bound here rather than none. A dumb byte count, on purpose: the bound is a resource limit, not a statement about what a document may contain.
Variables ¶
var ( ErrNilDocument = errors.New("sbom document is nil") ErrUnsupportedTarget = errors.New("unsupported sbom target") ErrUnsupportedFormat = errors.New("unsupported sbom format") ErrMalformedJSON = errors.New("malformed sbom json") // ErrDocumentTooLarge reports an input over MaxDocumentBytes, refused // before any of it is parsed. ErrDocumentTooLarge = errors.New("sbom document exceeds the size bound") // ErrSyftJSONUnsupported reports that the input is a syft-format JSON SBOM, // which Bomly does not ingest. Detection is kept so callers can point the // user at the conversion path instead of a generic format error. ErrSyftJSONUnsupported = errors.New("syft JSON SBOMs are not supported; convert with: syft convert <file> -o spdx-json") )
var ErrAmbiguousJSON = errors.New("ambiguous sbom json")
ErrAmbiguousJSON reports a document whose JSON does not have one unambiguous reading: it repeats an object member name, it contains bytes that are not valid UTF-8, or it escapes an unpaired UTF-16 surrogate, which names no character. The wrapped error says which, and where.
var ErrNilGraph = errors.New("dependency graph is nil")
var ErrUnverifiableJSON = errors.New("unverifiable sbom json")
ErrUnverifiableJSON reports a document Bomly declines to validate because checking it would cost more memory than the check is worth.
Functions ¶
func ComponentEcosystem ¶
ComponentEcosystem resolves the ecosystem a document component belongs to: the component's own ecosystem field when the SDK recognizes it, and otherwise the ecosystem named by its package URL's type.
It lives here, beside the other ingest identity rules, because the question is about Component -- this package's own type -- and because it used to be answered in more than one place. ToGraph resolved it inline, and the CLI's benchmark kept a twelve-row purl-type switch of its own that had already drifted away from the SDK's table: it answered Elixir for pkg:hex, which purlkit refuses precisely because Hex serves Elixir and Erlang alike and nothing in the PURL says which; and it had never learned hackage, cran, opam, deb, or otp, so Haskell, R, OCaml, Debian, and OTP components came back unknown. That is what a second table does -- it is correct the day it is written and quietly wrong afterwards.
The purl-type half of the answer is model.EcosystemForPURLType, the one table every consumer reads; this function adds only the component's own ecosystem hint on top of it.
func DocumentAssertionsFor ¶
func DocumentAssertionsFor(doc *Document) *model.DocumentAssertions
DocumentAssertionsFor returns the record of what a source document asserted about itself, for attaching to the graph entry that document became.
It is never nil for a document that was read, even one that asserted nothing. The presence of the record is itself a fact -- that this entry came from an SBOM -- and a merged export needs it: CycloneDX permits a document with neither a serial number nor metadata, and treating that as "no source" made a merge of two documents look like a conversion of one, publishing the merged inventory under the other document's identity.
Exported so the detector and this package agree by construction rather than by both remembering the same rule.
func IsProjectRootComponent ¶
IsProjectRootComponent reports whether a component is a synthesized pseudo root that stands in for the scanned project rather than a resolved package.
func MarshalDepGraphJSON ¶
func MarshalDepGraphJSON(g *model.Graph, target Target, buildOpts BuildOptions, encodeOpts EncodeOptions) ([]byte, error)
MarshalDepGraphJSON converts a dependency graph directly into a target JSON SBOM.
For a graph built from ingested SBOMs, prefer MarshalGraphEntriesJSON: see FromDepGraph for what this entry point cannot see.
func MarshalGraphEntriesJSON ¶
func MarshalGraphEntriesJSON(g *model.Graph, entries []model.GraphEntry, target Target, buildOpts BuildOptions, encodeOpts EncodeOptions) ([]byte, error)
MarshalGraphEntriesJSON converts the prepared graph entries and the graph they consolidated into directly into a target JSON SBOM.
Each call builds a new document with its own identity. To write one scan in several formats, build once with FromGraphEntries and call MarshalJSON per target, so every encoding carries the same UUID (see BuildOptions).
func MarshalJSON ¶
func MarshalJSON(doc *Document, target Target, opts EncodeOptions) ([]byte, error)
MarshalJSON renders the intermediate SBOM document to a target JSON format.
func ToGraphEntry ¶ added in v0.14.0
func ToGraphEntry(doc *Document, manifest model.ManifestMetadata) (model.GraphEntry, error)
ToGraphEntry converts a document into one manifest-scoped graph entry: the graph ToGraph builds, the document's own assertions, and -- carried in Packages, where consolidation folds them into the registry -- what the document asserted about its packages that a graph node cannot hold: the advisories it listed for a component, with their VEX analysis, and a component's end-of-life record. Before this, both were read from the document and dropped at this hop, so a document that said a package was vulnerable, or not affected, produced a graph that said neither.
Only components that carried one or the other get a package; the rest are what the graph already says. Two components that mint one identity fold into one package under the registry's merge classes. The slice is ordered by package URL.
Types ¶
type BuildOptions ¶
type BuildOptions struct {
DocumentName string
// DocumentNS pins the SPDX documentNamespace. Empty mints one in Bomly's
// form, "https://bomly.dev/spdx/<uuid>", sharing the serial's UUID.
DocumentNS string
ToolName string
ToolNames []string
ToolVersion string
Created time.Time
RootComponentID string
// SerialNumber pins the CycloneDX serialNumber, an urn:uuid. Empty mints
// one sharing the namespace's UUID.
SerialNumber string
// ProjectRoot, when non-nil, lets the projection synthesize a primary
// component for the scanned project when the graph has no single root.
ProjectRoot *ProjectRoot
Provenance Provenance
// Lifecycle is the CycloneDX lifecycle phase the document describes
// (for example "pre-build" for source scans, "post-build" for container
// images). Empty omits lifecycle metadata.
Lifecycle string
// Aggregate is the CycloneDX composition completeness declaration
// ("complete", "incomplete", ...). Empty omits the declaration; callers
// must only claim "complete" when nothing filtered or degraded the graph.
Aggregate string
// RestatesSource is true only when the export is a faithful restatement
// of its single source document: nothing filtered, enriched, or degraded
// the graph between ingest and export. Only then does a conversion adopt
// the source's identity, creation time, name and comment (ADR-0042). The
// default, false, is the safe side: the document mints its own identity
// and links the source. Ignored with zero or several sources, and
// overridden when a natively resolved entry sits beside the document:
// that graph is a merge whatever the caller says.
RestatesSource bool
// Registry, when non-nil, supplies matching-stage enrichment (licenses,
// vulnerabilities, CPEs, digests, EOL) resolved by PURL and folded onto
// each component during projection.
Registry *model.PackageRegistry
}
BuildOptions controls how a depgraph is projected into the intermediate SBOM model.
One build is one document. The identity slots left empty here are filled from a single UUID, so the SPDX namespace and the CycloneDX serial of the resulting Document name the same document; pinning either slot derives the other from it when it carries a UUID. A scan exported in two formats is therefore built once and encoded twice -- FromGraphEntries, then MarshalJSON per target. Marshalling the graph once per target builds twice and mints two identities (issue #109).
type Component ¶
type Component struct {
ID string
Name string
// Org is the package namespace -- an npm scope, a Go module host and
// owner, a Maven group. It is the PURL's namespace segment, emitted as
// CycloneDX `group`. SPDX 2.3 has no equivalent field, so there it is
// carried only inside the PURL external reference.
Org string
Version string
// Scopes is the full scope set, not a scalar. A package reachable from
// both a runtime and a development root carries both, and exporting only
// the merged precedence value lost that at the SBOM boundary -- which is
// the whole of what PR #406's scope union bought.
//
// Both formats hold one scope per component, so each encoder projects
// this onto its own scalar and writes the set beside it in a carrier the
// decoder prefers: a `bomly:scopes` CycloneDX property, a `scope=` field
// in the SPDX package comment. The projection, the carrier format, and
// the reverse mapping are all the SDK's (ADR-0037): they are one rule,
// and a second copy here is how the forward and reverse directions came
// to disagree in the first place.
//
// A source document's own scalar rides beside the set in SourceScope, so
// the derived set is never the only record of what the document said.
Scopes []model.Scope
// SourceScope is the scope word the source document used, in that
// document's own vocabulary -- CycloneDX's "optional", say. It is a
// preserved claim and never an input to filtering: Scopes is what the
// pipeline reads, and the SDK derives it from this.
//
// ADR-0037 asks that the word be re-emitted verbatim unless Bomly's own
// scope set changed, so "optional" and "excluded" do not collapse into
// "required" across a round trip that asserted neither. Whether the word
// still means what the set means is model.CycloneDXScopeForExport's
// decision, not this package's -- the mapping that read the word in is
// the only thing that can say whether it still describes the set, and a
// second copy of it here is how the two directions came to disagree
// before.
//
// Only CycloneDX has a scalar to hold it. SPDX 2.3 has no scope field at
// all, so an SPDX export carries the set in its comment carrier and the
// source word stops there.
SourceScope string
PURL string
Ecosystem string
PackageManager string
Type string
Copyright string
Licenses []License
// Matching-stage enrichment (populated when BuildOptions.Registry is set).
CPEs []string
Digests []Digest
Vulnerabilities []Vulnerability
EOL *EOL
// Where the package came from. A detector asserts at most one of
// ArtifactURL and VCSURL; registry enrichment may then fill VCSURL from a
// resolved source repository when it is empty, so a component enriched
// under --enrich can carry both. VCSRevision only accompanies VCSURL.
// Both are plain absolute http(s) URLs; composing them into a format's
// locator grammar is the encoder's job.
ArtifactURL string
VCSURL string
VCSRevision string
// What the source document asserted about this component, beyond its
// identity (ADR-0037, issue #396).
//
// These carry a foreign document's own claims through the graph so a
// conversion does not silently drop them: `bomly scan --sbom --format
// spdx` used to lose the supplier, description, checksums, CPEs and
// references its input stated, because the only things surviving the
// graph hop were coordinates, scope, copyright and licenses.
//
// They are SDK types rather than local structs on purpose. Each carries
// its own publication gate -- Contact.Normalized, ExternalReference
// .Normalized -- and every value crossing this boundary re-clears it,
// because an ingested document is untrusted input that Bomly re-emits.
// A local mirror of these shapes would be a second place for those rules
// to be forgotten, which is the defect ADR-0037 replaced.
Supplier *model.Contact
Originator *model.Contact
Description string
Homepage string
// ExternalReferences are the document's own references, kept with the
// category and type it stated so the SPDX triple round-trips without
// being re-derived. Merge class: set, unioned by the reference's own
// identity.
ExternalReferences []model.ExternalReference
// Every place this package was resolved from, primary first.
//
// ADR-0041 folds equal-identity records into one node and keeps their
// disagreement as a list of origins -- a package resolved from two
// different registries or mirrors is exactly the dependency-confusion
// signal that fold exists to preserve. The single-valued fields above
// carry the first, because a format has one download location; this
// carries all of them, so the evidence survives the export boundary
// rather than stopping at it.
Origins []ComponentOrigin
}
Component describes one package surfaced in the intermediate SBOM model.
type ComponentOrigin ¶
ComponentOrigin is one place a package was resolved from, already through the ADR-0033 publication gates.
type Dependency ¶
Dependency describes one package relationship list in the intermediate SBOM model.
type Document ¶
type Document struct {
Name string
Namespace string
Tool string
Tools []string
ToolVersion string
Created time.Time
SerialNumber string
// SerialVersion is the CycloneDX document revision that goes with
// SerialNumber. A serial names a BOM; the revision names which issue of
// it, so adopting a source's serial without its revision produced a
// document claiming to be revision 1 of a BOM whose revision 2 it had
// actually converted. Zero means unset and encodes as 1.
SerialVersion int
Provenance Provenance
Lifecycle string
Aggregate string
Components []Component
Dependencies []Dependency
Roots []string
// Described are the components the document names as its subject, as
// decoded: CycloneDX metadata.component, the targets of SPDX DESCRIBES
// and DESCRIBED_BY. Roots are the graph's entry points and can include a
// component the document never called its subject -- a disconnected
// package, say -- so the two are kept apart. Empty for a document
// projected from a graph, whose subject is its single root.
Described []string
// Assertions are the claims this document makes about itself: its
// identity, name, data license, creators, tools, and comment. A decoder
// fills them; ingest carries them onto the graph entry the document
// became, so a later export can say what the source said (ADR-0037).
//
// This is what a document says about *itself*, which is why it is not the
// same field as Sources below.
Assertions model.DocumentAssertions
// UnknownScopeTokens are the scope-carrier tokens an ingested document
// named that this build does not recognize, deduplicated and sorted.
//
// The carrier is Bomly's own, so an unreadable token is most likely one a
// newer Bomly wrote. The scopes beside it are still kept -- that is the
// point of the SDK's lenient read -- but the fact that something was not
// read is worth telling a user, and this package has no logger of its
// own. The SBOM detector is where it becomes a warning.
//
// Empty for a document Bomly wrote and for a document with no carrier at
// all.
UnknownScopeTokens []string
// Sources are the documents this one was built from, one per ingested
// SBOM, in entry order.
//
// Empty for a native scan: nothing was ingested, so Bomly's own document
// asserts everything itself. One source is the conversion case, where the
// document restates that source's assertions -- the fixed point issue
// #396 requires. Two or more is the merge case, where the document
// asserts its own aggregate identity and *links* each source rather than
// inheriting one, because both formats give a document exactly one
// identity and picking a source's would name a document that is not this
// one.
Sources []model.DocumentAssertions
}
Document is an intermediate, format-agnostic SBOM representation.
func FromDepGraph ¶
func FromDepGraph(g *model.Graph, opts BuildOptions) (*Document, error)
FromDepGraph builds a neutral SBOM document from a dependency DAG.
For a graph that came from ingested SBOMs, prefer FromGraphEntries: this entry point has no way to see what those documents said about themselves, and so exports a document that credits only Bomly.
func FromGraphEntries ¶
func FromGraphEntries(g *model.Graph, entries []model.GraphEntry, opts BuildOptions) (*Document, error)
FromGraphEntries builds a neutral SBOM document from the prepared graph entries and the consolidated graph they produced.
Both are passed, and neither is derived from the other. The graph is the one already selected for output -- consolidation renamed its identities and the scope filter decided what stays -- so re-merging the entries here would export a different graph than the rest of the command reports. The entries are here for the one thing only they carry: what each source document asserted about itself, which the merge into a single graph necessarily discards (ADR-0037).
func UnmarshalJSON ¶
UnmarshalJSON parses a target JSON SBOM into the intermediate document model.
The document is checked for an unambiguous reading before any codec sees it (ADR-0039). This is the one gate every ingest path shares, so a format added later inherits it rather than having to remember it.
func (*Document) CreatedOrNow ¶
CreatedOrNow returns the document timestamp in UTC, defaulting to the current time.
func (*Document) NameOrDefault ¶
NameOrDefault returns the document name or Bomly's default document name.
func (*Document) NamespaceOrDefault ¶
NamespaceOrDefault returns the document namespace or a generated Bomly namespace.
func (*Document) SerialVersionOrDefault ¶
SerialVersionOrDefault returns the CycloneDX document revision, defaulting to the first revision. CycloneDX numbers a document from 1, and a BOM-Link has to name a revision, so there is no "unset" to write.
func (*Document) ToolNamesOrDefault ¶
ToolNamesOrDefault returns all producing tool labels, defaulting to Bomly's tool label.
func (*Document) ToolOrDefault ¶
ToolOrDefault returns the producing tool name or Bomly's default tool label.
type EncodeOptions ¶
type EncodeOptions struct {
Pretty bool
}
EncodeOptions controls JSON output formatting.
type ProjectRoot ¶
ProjectRoot describes the scanned project so the projection can synthesize a primary component when the graph itself has no single root (multiple manifests, multiple ecosystems). Name is required; Version is optional.
type Provenance ¶
type Provenance struct {
Manufacturer string
SecurityContact string
VulnerabilityDisclosureURL string
SupportEnd string
}
Provenance carries optional producer metadata emitted into SBOM documents. The fields map onto EU CRA transparency expectations (manufacturer identification, security contact, coordinated disclosure, support period) but are format-agnostic and always optional.
func (Provenance) Empty ¶
func (p Provenance) Empty() bool
Empty reports whether no provenance field is set.
type Target ¶
type Target string
Target identifies an SBOM wire format target.
const ( TargetSPDX23JSON Target = "spdx-2.3+json" TargetCycloneDX14JSON Target = "cyclonedx-1.4+json" TargetCycloneDX15JSON Target = "cyclonedx-1.5+json" TargetCycloneDX16JSON Target = "cyclonedx-1.6+json" TargetCycloneDX17JSON Target = "cyclonedx-1.7+json" TargetSyftJSON Target = "syft+json" // recognized on ingest so the refusal can name the conversion path; never decoded, no codec )
func DetectJSONTarget ¶
DetectJSONTarget identifies the supported SBOM JSON format represented by data.
type Vulnerability ¶
type Vulnerability struct {
ID string
Source string
Severity string
Score *float64
Vector string
Method string
CWEs []int
FixedVersions []string
Advisories []string
Description string
// Recommendation is remediation guidance derived from known fixed
// versions (CycloneDX `recommendation`; SPDX 2.3 has no equivalent).
Recommendation string
// Analysis is the VEX impact analysis the source attached to this
// advisory for this component (CycloneDX `analysis`; SPDX 2.3 has no
// equivalent). The SDK type is used directly, as the other assertions
// are (ADR-0037): a projection would have to be re-gated on both sides.
Analysis *model.VulnerabilityAnalysis
}
Vulnerability is a format-agnostic projection of one matching-stage advisory affecting a component. Encoders map it to the format's native representation (CycloneDX vulnerabilities, SPDX SECURITY external references).