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.
go get github.com/git-pkgs/sbom
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:
}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)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.
| 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.
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.
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.
MIT.