sbom

package module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 13 Imported by: 2

README

sbom

Go library for reading and writing Software Bill of Materials documents. Reads CycloneDX JSON/XML and SPDX JSON/tag-value into a single Document / Package / Relationship model, and writes that model back out as CycloneDX (JSON or XML) or SPDX JSON.

Ported from github.com/andrew/sbom.

Installation

go get github.com/git-pkgs/sbom

Parsing

import "github.com/git-pkgs/sbom"

data, _ := os.ReadFile("bom.cdx.json")
doc, err := sbom.Parse(data)
if err != nil {
    log.Fatal(err)
}

fmt.Println(doc.Type)             // cyclonedx
fmt.Println(doc.SpecVersion)      // 1.6
fmt.Println(doc.Document.Name)    // my-app

for _, p := range doc.Packages {
    fmt.Println(p.Name, p.Version, p.PURL())
}

Parse auto-detects the format. Detect returns just the type without a full parse, scanning JSON keys, the XML root, or the tag-value document header.

Packages with different document IDs or PURLs remain separate, even when their names and versions match. AddPackage replaces an entry only when its name, version, document ID, and set of PURLs match. Package.PURL() returns the first PURL; Package.ExternalRefs retains all declared references.

switch sbom.Detect(data) {
case sbom.TypeCycloneDX:
case sbom.TypeSPDX:
}

Generating

Build an *sbom.SBOM, populate Document and Packages, then Encode:

s := sbom.New(sbom.TypeCycloneDX)
s.Document = sbom.Document{
    Name:      "my-app",
    Namespace: "https://example.com/my-app",
    Component: sbom.Component{
        Type:              "application",
        Name:              "my-app",
        Version:           "1.2.3",
        LicenseExpression: "MIT",
        LicenseNames:      []string{"Additional Project Terms"},
        ExtractedLicenses: []sbom.ExtractedLicense{{
            Name: "LICENSE.custom",
            Text: "Custom license text",
        }},
    },
    Creators:  []sbom.Creator{{Type: "Tool", Name: "my-tool-1.0"}},
}
s.AddPackage(sbom.Package{
    Name: "lodash", Version: "4.17.21", LicenseDeclared: "MIT",
    ExternalRefs: []sbom.ExternalRef{{Type: "purl", Locator: "pkg:npm/lodash@4.17.21"}},
})

sbom.Encode(os.Stdout, s, sbom.FormatCycloneDXJSON)
sbom.Encode(os.Stdout, s, sbom.FormatSPDXJSON)

Relationships

Both parsers normalise dependency edges to sbom.RelDependsOn and SPDX's document-to-root link to sbom.RelDescribes. ClassifyScope walks that graph to tag each package as a direct or transitive dependency of the document's root:

doc, _ := sbom.Parse(data)
scope := doc.ClassifyScope()
for _, p := range doc.Packages {
    switch scope[p.ID] {
    case sbom.ScopeDirect:
    case sbom.ScopeTransitive:
    default:
        // root, or a flat-list SBOM with no dependency graph
    }
}

ClassifyScope returns nil when the document has no dependency graph at all, so callers can hide a scope filter rather than showing every package as unclassified.

Supported formats

Format Parse Encode Notes
CycloneDX JSON yes yes nested components flattened, license.id / license.name / expression all read
CycloneDX XML yes yes nested components, dependency references, and package metadata
SPDX JSON yes yes {"sbom": ...} and in-toto {"predicate": ...} envelopes unwrapped on parse
SPDX tag-value yes no document and package fields, external references, relationships, and multiline text

SPDX file, snippet, annotation, and extracted-license records are not included in the parsed model. YAML parsing is not supported.

Why not protobom?

protobom is the OpenSSF normaliser and covers every serialisation. It's pure Go (no cgo) but importing pkg/reader adds about 21 indirect modules including google.golang.org/protobuf, sigs.k8s.io/release-utils, logrus, tablewriter, and a handful of terminal-colour libraries that have no business in a parser. This module has zero dependencies and the field mappings were already worked out in the Ruby gem, so for tools that just need the package list it's a smaller surface to audit. If you need full spec coverage, use protobom.

Benchmarks

go test -bench . -benchmem

On an M1 Pro, Parse runs at roughly 100-165 MB/s; the 3 MB syft-generated nginx.spdx.json (151 packages) parses in about 18 ms. Detect is sub-microsecond on the same file.

License

MIT.

Documentation

Overview

Package sbom parses Software Bill of Materials documents into a unified data model. It reads CycloneDX JSON/XML and SPDX JSON/tag-value and normalises both into the same Document/Package/Relationship shape so callers don't need to care which format they were handed.

The model and field mappings are a port of github.com/andrew/sbom (Ruby).

doc, err := sbom.Parse(data)
for _, p := range doc.Packages {
    fmt.Println(p.Name, p.Version, p.PURL())
}

Index

Constants

View Source
const (
	SupplierOrganization = "Organization"
	SupplierPerson       = "Person"
)

Supplier/originator type values, shared across both formats.

View Source
const (
	RelDependsOn = "DEPENDS_ON"
	RelDescribes = "DESCRIBES"
)

Relationship type values. SPDX has ~40 of these; we name the two the parsers emit and ClassifyScope reads. CycloneDX dependency edges are normalised to RelDependsOn.

View Source
const (
	ScopeDirect     = "direct"
	ScopeTransitive = "transitive"
)

Scope values returned by ClassifyScope.

Variables

View Source
var ErrUnrecognized = errors.New("sbom: unrecognized format")

ErrUnrecognized is returned when the input does not look like any supported SBOM format.

Functions

func Encode

func Encode(w io.Writer, s *SBOM, f Format) error

Encode writes s to w in the requested Format. Document fields left empty are filled with spec-mandated defaults (timestamps, NOASSERTION, etc.).

Types

type Checksum

type Checksum struct {
	Algorithm string
	Value     string
}

Checksum is a single hash over a package artefact.

type Component

type Component struct {
	Type              string
	Name              string
	Version           string
	LicenseExpression string
	LicenseNames      []string
	ExtractedLicenses []ExtractedLicense
}

Component is the root subject the SBOM describes (CycloneDX metadata.component / SPDX root package).

type Creator

type Creator struct {
	Type string
	Name string
}

Creator is a tool, person, or organization that produced the document.

type Document

type Document struct {
	Name        string
	ID          string
	Type        Type
	SpecVersion string
	DataLicense string
	Namespace   string
	Created     string
	Supplier    string
	Creators    []Creator
	Component   Component
}

Document holds metadata about the SBOM document itself, distinct from the packages it describes.

type ExternalRef

type ExternalRef struct {
	Category string
	Type     string
	Locator  string
}

ExternalRef is a typed pointer to an external resource. PURLs and CPEs are stored here in both formats.

type ExtractedLicense added in v0.1.4

type ExtractedLicense struct {
	ID   string
	Name string
	Text string
}

ExtractedLicense is non-SPDX license text associated with a component. ID is optional. Encode prefixes a supplied ID with LicenseRef- when needed, or generates a stable LicenseRef when ID is empty.

type Format

type Format int

Format selects an output serialisation for Encode.

const (
	FormatCycloneDXJSON Format = iota
	FormatCycloneDXXML
	FormatSPDXJSON
)

type Package

type Package struct {
	ID               string
	Name             string
	Version          string
	Type             string
	Description      string
	Supplier         string
	SupplierType     string
	Originator       string
	OriginatorType   string
	Homepage         string
	DownloadLocation string
	Filename         string
	LicenseConcluded string
	LicenseDeclared  string
	Copyright        string
	Checksums        []Checksum
	ExternalRefs     []ExternalRef
	Properties       []Property
}

Package is a single component/package entry from the SBOM, regardless of source format.

func (*Package) CPE

func (p *Package) CPE() string

CPE returns the first CPE identifier for this package, if any.

func (*Package) PURL

func (p *Package) PURL() string

PURL returns the Package URL for this package, if one was declared. In CycloneDX this is the top-level purl field; in SPDX it lives in externalRefs with referenceType "purl". Both are normalised into ExternalRefs so this is a simple lookup.

type Property

type Property struct {
	Name  string
	Value string
}

Property is an arbitrary name/value annotation on a package.

type Relationship

type Relationship struct {
	SourceID string
	Source   string
	TargetID string
	Target   string
	Type     string
}

Relationship links two elements in the SBOM. SourceID/TargetID are the raw identifiers from the document; Source/Target are resolved names where the parser could determine them.

type SBOM

type SBOM struct {
	Type          Type
	SpecVersion   string
	Document      Document
	Packages      []Package
	Relationships []Relationship
	// contains filtered or unexported fields
}

SBOM is the unified parse result.

func New

func New(t Type) *SBOM

New returns an empty SBOM ready for AddPackage calls. Use this when building a document for Encode rather than parsing one.

func Parse

func Parse(data []byte) (*SBOM, error)

Parse reads CycloneDX JSON/XML or SPDX JSON/tag-value, detecting the format.

func (*SBOM) AddPackage

func (s *SBOM) AddPackage(p Package)

AddPackage appends p, replacing any existing package with the same name, version, document ID, and set of PURLs.

func (*SBOM) ClassifyScope added in v0.1.3

func (s *SBOM) ClassifyScope() map[string]string

ClassifyScope derives direct-vs-transitive scope for each package from the relationship graph. Roots are nodes that originate DEPENDS_ON edges but are never themselves a DEPENDS_ON target, plus anything a DESCRIBES edge points at (SPDX's document-to-root-package link). A package is ScopeDirect if a root depends on it, ScopeTransitive if only a non-root does, and absent from the map if the graph doesn't mention it at all. Returns nil for a flat-list SBOM with no dependency graph so callers can tell "no graph" apart from "graph present but this package is a root".

func (*SBOM) FilterProperties added in v0.1.2

func (s *SBOM) FilterProperties(keep func(name string) bool)

FilterProperties walks every package and removes properties whose names keep returns false for. Useful when handing a document to a downstream consumer that doesn't recognise a particular property namespace — for example, strip a tool-specific "mytool:" prefix before sharing outside the team that produced it. The filter mutates the SBOM in place; callers wanting a copy should Encode + Parse around the call.

Document- and Component-level metadata is untouched; only the per-package Properties slice is filtered.

type Type

type Type string

Type identifies the source SBOM specification.

const (
	TypeUnknown   Type = ""
	TypeCycloneDX Type = "cyclonedx"
	TypeSPDX      Type = "spdx"
)

func Detect

func Detect(data []byte) Type

Detect inspects content and returns the SBOM Type without fully parsing it. Returns TypeUnknown if the format can't be determined.

It scans JSON keys, the XML root, or the tag-value document header.

Jump to

Keyboard shortcuts

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