Safeguard
Supply Chain Security•

protobom and bomctl Explained: Format-Neutral SBOM Handling

protobom is an OpenSSF data model and Go library that reads and writes SPDX and CycloneDX; bomctl is its CLI for fetching, merging and exporting SBOMs.

Safeguard Security Team
41 min read

protobom is an OpenSSF project that defines one format neutral data model for SBOMs, written in protocol buffers, plus a Go library that reads SPDX and CycloneDX documents into that model and writes them back out. bomctl is a separate OpenSSF command line tool built on protobom that fetches, caches, merges, links and exports many SBOMs at once. Together they let you treat SBOMs as one graph of software, whatever format each supplier sends.

Both are Apache 2.0 licensed OpenSSF sandbox projects that grew out of a United States government push to make SBOMs cheaper to produce and consume. We checked every version, date and command below against the projects' own repositories, documentation and release notes on 25 October 2026.

Key takeaways

  • protobom is a data model and a library, not a new SBOM format. It gives programs one in memory graph of nodes, typed edges and root elements for both SPDX and CycloneDX.
  • Format support moved in 2026. protobom v0.6.0 (26 August 2026) added SPDX 3.0.1, SPDX 2.2 and tag value support. The README's support table still marks SPDX 3 as planned, so read the release notes and the code, not only the table.
  • Conversion still loses data. The project documents that loss happens during serialization, and its scope excludes vulnerability, provenance and VEX data. Format neutral does not mean lossless in every direction.
  • bomctl works on a local SQLite cache. You fetch or import SBOMs into the cache, operate on them there (alias, tag, link, merge, list) and then export or push them. Files on disk are never modified in place.
  • bomctl is experimental. Its README says so, its latest tagged release is v0.4.3 from 2 May 2025, and diff, split, redact, trim and enrich are still on the roadmap.

What is protobom, and who started it?

protobom describes itself as a protocol buffers representation of SBOM data that can ingest documents in modern SPDX and CycloneDX versions, with an accompanying Go library generated from that definition. In practice it is three things that ship together:

  1. A schema file, api/sbom.proto, that defines messages such as Document, Metadata, NodeList, Node, Edge, Person, Tool and ExternalReference.
  2. Generated Go types for those messages, with helper methods for building and walking the graph.
  3. Readers ("unserializers") that turn SPDX or CycloneDX into a protobom document, and writers ("serializers") that render a protobom document back into a standard format.

Where it came from

The design proposal in the repository, dated 16 May 2023 and addressed to the DHS SBOM cohort, sets out the original plan: a universal protocol buffers structure that captures data from any SBOM format, read and write libraries in Go, a command line converter, and a documented "degradation strategy" that explains how data is lost when rendering from the neutral model into each format. Its background section is blunt: translation between SBOM formats loses data, and most early tools reduced an SBOM to a flat dependency list.

On 16 April 2024 the OpenSSF announced the launch of protobom together with the Cybersecurity and Infrastructure Security Agency (CISA) and the Department of Homeland Security Science and Technology Directorate (DHS S&T). According to that announcement, CISA and the DHS S&T Silicon Valley Innovation Program funded a cohort of seven startups to develop protobom, and the OpenSSF committed to hosting its open development. The project's technical charter was adopted on 12 April 2024, establishing protobom as a Series of LF Projects, LLC with a Technical Steering Committee drawn from its committers.

What problem a format neutral model solves

Two SBOM standards dominate. SPDX is a Linux Foundation project and an international standard (ISO/IEC 5962:2021), with SPDX 3.0 now the current version on spdx.dev. CycloneDX is an OWASP project, standardised at Ecma International as ECMA-424, and its current version is 1.7, released on 21 October 2025 with JSON, XML and protobuf serializations.

The two formats describe the same world with different shapes. SPDX 2.x lists packages and files and connects them with a flat list of typed relationships. CycloneDX nests components inside components and keeps a separate dependency graph. SPDX 3 moves to a graph of elements and relationship objects. Field names, identifier rules and licence handling differ in each.

Every SBOM consumer therefore maintains several parsers, or converts everything to one format and hopes nothing important falls out. The OpenSSF launch announcement frames protobom as a format neutral data layer on top of the standards. A tool author writes against Node, Edge and Metadata once, and the library deals with which standard the bytes arrived in.

That matters more in 2026 than it did in 2023. CISA's 2026 Minimum Elements for a Software Bill of Materials, published on 29 July 2026, replace the NTIA 2021 minimum elements and add new fields such as SBOM Author Signature, SBOM Data Format Name, SBOM Data Format Version, SBOM Generation Context, SBOM Tool Name and Version, Component Hash Value and Algorithm, and Component License. The guidance says an element need not match one particular field in a format. A buyer checking SBOMs from fifty suppliers has to do that mapping somewhere, and a neutral model is a sensible place to do it once.

How the protobom data model works

Open api/sbom.proto and the structure is short enough to hold in your head.

Document and metadata

A Document has exactly two fields: metadata and node_list. The Metadata message carries the document's own identity and provenance: an id, a version, a name, a date timestamp, a list of tools (each with name, version and vendor), a list of authors (each a Person that can be a person, an organisation or a software agent), a comment, a list of document types and a source_data record.

Two parts of the metadata deserve attention.

Document types. The DocumentType message has an SBOMType enum with values DESIGN, SOURCE, BUILD, ANALYZED, DEPLOYED, RUNTIME, DISCOVERY, DECOMISSION (spelled that way in the schema) and OTHER. The comments map these to CycloneDX lifecycle phases: design, pre build, build, post build and operations. This is where the 2026 CISA element "SBOM Generation Context" naturally lands, because CISA describes it as the lifecycle phase at which the SBOM was produced, giving "before build", "build" and "after build" as examples.

Source data. SourceData records the original format string (for example text/spdx+json;version=2.3), hashes of the original document, its size in bytes and, optionally, its URI. This is how a protobom document remembers what it was before it was parsed, which bomctl relies on to give you back the original bytes.

Nodes

A Node is either a package or a file (the NodeType enum). Its fields cover most of what SPDX packages and CycloneDX components carry:

  • Identity: id, name, version, file_name.
  • Locations: url_home, url_download, and a list of external_references.
  • Licensing: licenses (a list), license_concluded, license_comments, copyright, attribution.
  • Descriptions: summary, description, comment, source_info.
  • People: suppliers and originators, both lists of Person.
  • Dates: release_date, build_date, valid_until_date.
  • Integrity and identifiers: hashes, a map from a HashAlgorithm enum to the digest, and identifiers, a map from a SoftwareIdentifierType enum to the value.
  • Purpose: primary_purpose, a list drawn from an enum that includes APPLICATION, LIBRARY, FRAMEWORK, CONTAINER, OPERATING_SYSTEM, FIRMWARE, DEVICE, DEVICE_DRIVER, MACHINE_LEARNING_MODEL, DATA, SOURCE, PATCH and others.
  • Extensions: properties, a list of name and data pairs, plus content_type and file_kind for files.

One detail is easy to miss. The SoftwareIdentifierType enum lists only PURL, CPE22, CPE23 and GITOID. CISA's 2026 guidance asks for at least one software identifier and names CPE and Package URL (PURL) first, then allows UUIDs, organisation specific identifiers, commit hashes, OmniBOR (which is built on gitoids) and the Software Hash Identifier (SWHID). If your suppliers use SWHIDs or internal identifiers, check how those survive the trip into protobom before you rely on them downstream.

Edges and root elements

Relationships are Edge messages. Each edge has a type, a single from node ID and a list of to node IDs, so one edge can express "this package depends on these twelve packages". The Edge.Type enum is large, with 44 named relationship types plus UNKNOWN. It includes contains, contained_by, dependsOn, dependencyOf, buildDependency, devDependency, runtimeDependency, optionalDependency, testDependency, describes, describedBy, generates, generatedFrom, staticLink, dynamicLink, variant, patch, ancestor, descendant, expandedFromArchive, fileAdded, fileModified, fileDeleted and other. If those names look familiar, it is because most of them follow SPDX 2.x relationship types, which have the richest vocabulary of the two formats.

NodeList ties it together: a repeated list of nodes, a repeated list of edges and a list of root_elements, the IDs of the nodes the document is about. In an SPDX document those are what the document DESCRIBES. In CycloneDX the root is usually the component in metadata.component.

For tool authors, the point is that an SPDX package and a CycloneDX component both arrive as a Node, so the same code reads either (Step 8 below shows the README's example).

How the formats map onto the model

The table below summarises the mapping in plain terms. It is our reading of the schema comments, the serializer documentation and the translation analysis notes in the repository, not an official crosswalk; for field by field detail between SPDX 3 and CycloneDX 1.7 see our SPDX 3 to CycloneDX 1.7 field mapping.

protobom conceptSPDX 2.xSPDX 3.0.1CycloneDX 1.4 to 1.7
Document metadataDocument creation information (creators, created, namespace)SpdxDocument and CreationInfometadata block (authors, tools, timestamp)
Node of type PACKAGEPackagesoftware_Packagecomponent
Node of type FILEFilesoftware_Filecomponent of type file
Edge containsCONTAINS relationshipRelationship of type containsnested components
Edge dependsOnDEPENDS_ON relationshipRelationship of type dependsOndependencies entry
Root elementsDESCRIBES relationship from the documentrootElementmetadata.component
Hashes mapPackageChecksum, FileChecksumverifiedUsing hasheshashes
Identifiers mapexternal refs (purl, cpe22Type, cpe23Type, gitoid)packageUrl and externalIdentifierpurl, cpe, omniborId
Document typenot native in 2.xsoftware_sbomTypemetadata.lifecycles

Which versions are supported today

This is where the repository disagrees with itself, so be precise.

The README's support table, fetched on 25 October 2026, lists SPDX 2.2 and 2.3 (JSON and tag value) and CycloneDX 1.4 to 1.7 (JSON) as supported for read and write, and SPDX 3.0 and 3.1 as planned.

The code and release notes say more. The v0.6.0 release (26 August 2026) states it adds full support for SPDX 3.0.1. The main branch's pkg/formats/formats.go includes SPDX 3.0.1 JSON in its supported list, the companion sbom-convert README lists spdx-3.0.1 as an output format, and v0.6.2 (21 September 2026) fixed SPDX 3 output correctness.

Our reading: SPDX 3.0.1 JSON is supported from v0.6.0, the README table has not caught up, and SPDX 3.1 is not yet supported. CycloneDX XML and protobuf encodings are not in the library's support list; JSON is the encoding to plan around. If you depend on a specific version, pin the library version in go.mod and test with your own documents.

What conversion loss remains, even with a neutral model

The honest answer is "some, and the project says so". Several kinds of loss remain.

Loss the project designed for

The serializer documentation states that any data loss or modification due to conversion happens during serialization, that is, when rendering from the neutral model into a specific format and version. Reading into protobom aims to be lossless; writing out cannot always be, because each target format has fields the others lack. The original proposal called for a documented degradation strategy, and the serializer guide notes that loss points are marked in the code with comments that should feed that document.

Loss by scope

The proposal's non goals include support for each format's non SBOM features, naming vulnerability data, provenance and VEX. CycloneDX documents can carry vulnerabilities, formulation, services, annotations and much more. SPDX 3 has security, build, AI and dataset profiles. Those are outside what protobom is trying to carry. If your CycloneDX file includes a vulnerabilities array, do not expect it to come out the other side of a protobom round trip.

The proposal also rules out "clever hacks", such as stuffing data into comments or annotations to avoid loss. That is a good principle, and it means data with no home in the target format is dropped rather than smuggled.

Loss from structural differences

Even for core inventory data, the shapes differ:

  • Nesting versus relationships. CycloneDX nests components; SPDX 2.x uses CONTAINS relationships. Converting back and forth can flatten or restructure the tree. The CycloneDX 1.4 serializer described in the docs first creates components from nodes, then reads the edges and either nests nodes in the component tree or records them as dependencies, depending on the edge type.
  • Relationship vocabulary. SPDX 2.x has many relationship types; CycloneDX expresses fewer directly. A buildDependency or devDependency edge may come out as a plain dependency, or as a component scope, depending on serializer and version.
  • Document identity. The translation analysis notes show that when converting CycloneDX to SPDX 2.3, the SPDX documentNamespace is built from the name plus a fresh random UUID, and dataLicense is set to CC0-1.0 as SPDX requires. A converted document gets a new identity every run. Any process that compares document IDs across runs will see noise.
  • External document references. SPDX external document references point at other SPDX documents; CycloneDX externalReferences mean something broader. The analysis notes say the converter does not populate one from the other.

Loss from bugs, and how fast they get fixed

The release notes show loss is a moving target. v0.6.0 fixed an SPDX serializer that dropped packages with more than one primary purpose, preserved CycloneDX component scope, made licence expressions serialize as expressions rather than licence IDs, and preserved SPDX 2.3 file licence information. v0.6.2 fixed SPDX 3 output and kept the SPDX 2 namespace. Each was real loss in an earlier version, so test round trips with the exact library version your tools embed, and re-test when you upgrade.

Loss that is not conversion at all

Conversion cannot add what was never there. If a generator emitted packages without versions, without PURLs, without hashes or without any dependency edges, the converted document has the same holes in a different syntax. CISA's 2026 guidance asks SBOM authors to say explicitly when information is unknown, rather than leaving fields silently empty. A converter cannot tell the difference between "unknown" and "the generator did not bother".

What is bomctl?

bomctl's README calls it format agnostic SBOM tooling intended to bridge the gap between SBOM generation and SBOM analysis tools. Where protobom is a library for programmers, bomctl is a command line tool for handling many SBOMs that together describe a system.

The OpenSSF announced bomctl as a sandbox project under the Security Tooling Working Group on 5 September 2024. According to that announcement, it addresses two challenges raised at the 2023 Secure Open Source Software Summit: working across SBOM formats, and representing complex systems such as a Helm chart deployment made of a chart and several container images.

Two scoping choices shape everything bomctl does:

  1. It works on the shared subset. The README says bomctl is opinionated about supporting only the NTIA minimum fields or other fields protobom supports. The September 2024 announcement adds that it is not intended to fully support CycloneDX and SPDX. The NTIA 2021 list has since been replaced by CISA's 2026 Minimum Elements, so read that line as history; the principle is "fields that map cleanly between formats".
  2. It is experimental. The README carries a note saying it is an experimental project under active development. Treat it as a tool for exploring and scripting, not a system of record.

Which operations bomctl supports

The README groups the commands by where data flows.

StageCommandWhat it does (per the README)
Into the cachefetchRetrieves SBOMs over HTTPS, OCI, Git, GitHub and GitLab, and recursively loads SBOMs referenced as external references
Into the cacheimportLoads SBOM files from standard input or the local file system
In the cachealiasSets, lists or removes a single, globally unique readable name for a document
In the cachetagAdds, lists, removes or clears tags on a document
In the cachelinkAdds, lists, removes or clears links from a document or node to a document
In the cachelistLists cached documents, optionally filtered by tag
In the cachemergeMerges specified cached documents into a new document, with a name, alias and tags
Out of the cacheexportWrites stored SBOMs to the file system in a chosen format and encoding
Out of the cachepushSends a stored SBOM to a remote URL or file system path, optionally the whole external reference tree

Fetch and push can authenticate with a .netrc file. GitLab access uses a token in the BOMCTL_GITLAB_TOKEN environment variable; fetch uses GitLab's dependency list export API and push uses its generic package registry. Fetching a GitHub repository URL retrieves that repository's most recent dependency graph SBOM.

The README's command overview omits link, but the command has its own README section and is registered in the source.

What bomctl does not do yet matters as much. The README lists diff, split and redact as future manipulation commands, and lists interfacing with GUAC and Sigstore as future work. The roadmap adds trim, enrich (using the Transparency Exchange API and GUAC), verify, sign and visualize. The architecture decision record for diff, dated 30 July 2024, is still marked "Proposed".

OCI fetch in practice

The README names OCI among the fetch protocols but does not show an OCI example. The client code in internal/pkg/client/oci accepts URLs with an oci:// or docker:// scheme (and treats a missing scheme as OCI), resolves a tag or a digest, looks through the image manifest's layers for an SBOM, and returns an error listing the candidate digests if it finds more than one, asking you to put a specific digest in the fetch URL. We describe that behaviour from the source rather than giving a command line, because the README does not document one; check bomctl fetch --help on your installed version.

How bomctl stores SBOMs locally

The README calls the cache fundamental: files are not modified directly, the cache is. Fetching or importing parses a document with protobom and stores the result.

From the source:

  • The cache is a SQLite database file named bomctl.db.
  • By default it lives in a bomctl directory under your operating system's user cache directory (as returned by Go's os.UserCacheDir). On Linux that is usually under ~/.cache, and on macOS under ~/Library/Caches.
  • You can point it elsewhere with the global --cache-dir flag, which is useful for isolated pipeline runs.
  • A configuration file named bomctl (YAML) is read from your user configuration directory, or from a path given with --config.
  • Storage goes through protobom's storage module and an entity framework over SQLite, so documents are stored parsed rather than as loose files.

An accepted decision record dated 16 October 2024 adds an important behaviour. Each cached document also keeps its original bytes and format string. If a document is unchanged and you export it in its original format, bomctl writes back the original content byte for byte, and the original format value forces that. The stated reason is signatures: re-serializing a signed SBOM would break its signature.

How merge works

The merge decision record, dated 29 July 2024 and accepted, describes the intent: take one or more SBOMs, in different formats, and produce a single SBOM, merging components that match on different identifiers such as PURLs or hashes, and merging their dependencies and properties. The initial implementation uses protobom's own augmenting and merging. The record's investigation notes describe a union of node lists, a new root node built from the old root nodes, updated edges pointing at the new root, and de-duplicated authors, document types and tools. Fuzzy matching on components is listed as a future capability, and the record openly asks which document's data should win when the same field differs.

protobom v0.6.2, released on 21 September 2026, added Dedupe and Absorb functions and NodeList.Graft and GraftInto methods to its graph API, which are exactly the primitives a better merge needs. bomctl's go.mod on its main branch pins protobom v0.5.0, so bomctl does not yet benefit from them.

How link works, and why it matters for coverage

The link decision record, dated 18 September 2024, is accepted but its decision section still reads "TBD", with open questions about what a child to parent reference should look like and whether references are rewritten on push. The link command exists, but treat its exact semantics as version specific.

Linking is more than a convenience. CISA's 2026 Coverage element says an SBOM should include all components, including transitive dependencies, with no minimum depth, and that linking to other SBOM documents, for example one generated for a subcomponent, can help meet Coverage, provided the recipient has access to all linked SBOMs. A container image SBOM that links to the SBOM of the application inside it is a legitimate way to describe a system. bomctl's recursive fetch, which follows external references and loads the referenced SBOMs into the cache, is how a recipient makes sure they actually have the whole tree.

Format support in bomctl

bomctl's export and push accept these format values: original, spdx, spdx-2.3, cyclonedx, and cyclonedx-1.0 through cyclonedx-1.6. Encoding is JSON by default, and XML is accepted for CycloneDX only. There is no SPDX 3 or CycloneDX 1.7 output option in the README, which fits bomctl pinning an older protobom. If you need SPDX 3.0.1 or CycloneDX 1.7, the protobom library directly, or the sbom-convert tool built on it, is the route today.

Timeline

DateEventSource
16 May 2023Draft SBOM translation proposal addressed to the DHS SBOM cohortprotobom repository, docs/format-translation-library-proposal.md
10 Jan 2024bomctl GitHub repository createdGitHub API
12 Apr 2024protobom technical charter adopted as a Series of LF Projectsprotobom docs/CHARTER.md
16 Apr 2024OpenSSF, CISA and DHS S&T announce the protobom launchOpenSSF press release
5 Sep 2024bomctl announced as an OpenSSF sandbox projectOpenSSF blog
2 May 2025bomctl v0.4.3, the latest tagged release as of 25 Oct 2026GitHub releases
21 Oct 2025CycloneDX 1.7 releasedCycloneDX specification overview
29 Jul 2026CISA publishes 2026 Minimum Elements for an SBOM, replacing NTIA 2021CISA
26 Aug 2026protobom v0.6.0 adds SPDX 3.0.1, SPDX 2.2 and tag value supportGitHub releases
21 Sep 2026protobom v0.6.2 adds Dedupe, Absorb and Graft graph functionsGitHub releases

Where protobom and bomctl fit next to other SBOM tools

It helps to think of the SBOM life cycle in five steps: generate, sign and attest, distribute, store and relate, analyse and act. protobom and bomctl live in the middle.

Generators produce SBOMs from source trees, build systems, container images or binaries. protobom's proposal lists SBOM generation as a non goal. You still need a generator, and its output quality sets the ceiling for everything downstream.

Signing and attestation proves who produced an SBOM and that it has not changed. Sigstore's documentation covers keyless signing of blobs and containers and in-toto attestations. bomctl does not sign or verify SBOMs today; both are on its roadmap. What it does do is preserve original bytes so that a signature made upstream can still be verified after the document has passed through the cache. CISA's 2026 guidance adds SBOM Author Signature as a new element, so signing is becoming expected.

Graph and aggregation tools such as GUAC (Graph for Understanding Artifact Composition), another OpenSSF project, ingest SBOMs, attestations and other metadata and map relationships across all of them so you can query, for example, every artifact that includes a given package. That is a bigger job than bomctl's cache. bomctl's roadmap lists interfacing with GUAC under enrich. Our GUAC v1.0 explainer covers that layer.

SBOM management and analysis platforms store SBOMs per product and release, match components against vulnerability data, apply policy, track licences and report. bomctl's README describes its role as bridging generation and analysis, not replacing analysis.

Comparison: three ways to handle SBOMs across formats

protobom and bomctlFormat specific convertersFull SBOM platforms
ScopeRead, model, merge, link and write SBOM inventory data across SPDX and CycloneDXTranslate one document from one format to anotherIngest, store, analyse, apply policy and report across a portfolio
Data modelNeutral protobuf graph of nodes and typed edgesUsually direct field mapping between two schemasProduct specific internal model
Storagebomctl: local SQLite cache per user or pipelineNone; file in, file outCentral database with history and access control
Mergebomctl merge on cached documents; protobom graph functionsRarelyUsually at product or release level
Linking documentsRecursive fetch of external references; link commandNoVaries; often via product hierarchy
Vulnerability dataOut of scopeSometimes carried, sometimes droppedUsually central
Best forBuilding tools, normalising inputs, scripting multi document workflowsOne off conversions to meet a customer's format requestOngoing operations, compliance evidence and remediation

The categories overlap. A platform may use a neutral model internally; a pipeline may use bomctl to gather and normalise documents before handing them to a platform. The useful question is which job you need done, not which category wins.

Serialization still matters

protobom is protobuf inside, but it reads and writes SPDX and CycloneDX JSON (and SPDX tag value). CycloneDX's own protobuf serialization is a different thing: one format's binary encoding, not a cross format model. For choosing an exchange encoding, see our post on CycloneDX JSON, XML or protobuf walks through that choice.

Step by step: try bomctl and protobom on your own SBOMs

The commands below come from the protobom, bomctl and sbom-convert READMEs. We did not run them for this article. Where we fill in placeholders such as document IDs or file names, the flags are the ones each README documents. Run them first on test data and check --help on your installed version, because bomctl is experimental and flags can change.

Step 1: install bomctl

The README offers three routes. With Homebrew:

brew tap bomctl/bomctl && brew install bomctl

With the container image from Docker Hub:

docker run bomctl/bomctl:latest --help

Or from source with Go, Git and either Make or Task, using the build target for your platform (for example make build-macos-apple or make build-linux-amd):

git clone https://github.com/bomctl/bomctl.git
cd bomctl

Step 2: verify what you installed

bomctl signs its container images and release artifacts with keyless cosign signing. The README gives this check for the image:

cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp 'https://github\.com/bomctl/bomctl/\.github/.+'  bomctl/bomctl:latest

And this one for a downloaded release artifact, where ${artifact} is the file name:

cosign verify-blob --certificate ${artifact}-keyless.pem --signature ${artifact}-keyless.sig --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp 'https://github\.com/bomctl/bomctl/\.github/.+'  ${artifact}

A result of Verified OK means the signature checks out.

Step 3: fetch an SBOM from a URL

The README's example fetches a CycloneDX SBOM for the bomctl v0.3.0 container image from the project's playground repository. That SBOM has an external reference to the SBOM of the application inside the image, and bomctl fetches it recursively:

bomctl fetch https://raw.githubusercontent.com/bomctl/bomctl-playground/main/examples/bomctl-container-image/bomctl_bomctl_v0.3.0.cdx.json

To pull the latest dependency graph SBOM for a GitHub repository instead (private repositories need credentials through --netrc):

bomctl fetch https://github.com/bomctl/bomctl

Step 4: import a local SBOM and give it a name

If your second SBOM is a file on disk, for example one your build produced, import it. The README's synopsis is bomctl import [flags] { - | FILE...} with --alias and --tag flags. Filled in with our placeholder names:

bomctl import --alias app --tag release-2026-10 app.spdx.json

Then list what is in the cache to get document IDs and aliases:

bomctl list

Step 5: merge two SBOMs

The README's synopsis is bomctl merge [flags] DOCUMENT_ID... with --name, --alias and --tag. With placeholder IDs taken from bomctl list:

bomctl merge --name my-system --alias my-system DOCUMENT_ID_1 DOCUMENT_ID_2

Merging works on the cache, so your original files are untouched. Before you trust the result, check how many nodes it has compared with the inputs, and look for the same package appearing twice under different IDs (see Common mistakes below).

Step 6: export to the other format

The README's synopsis is bomctl export [flags] SBOM_ID... with --format, --encoding and --output-file. To write the merged document as SPDX 2.3 JSON and as CycloneDX 1.6 JSON:

bomctl export --format spdx-2.3 --output-file my-system.spdx.json my-system
bomctl export --format cyclonedx-1.6 --output-file my-system.cdx.json my-system

To hand a supplier's document on exactly as received, use --format original.

If you need SPDX 3.0.1 or CycloneDX 1.7, which bomctl's export does not list, the protobom team's sbom-convert tool does. Its README shows:

sbom-convert convert sbom.spdx.json -o sbom.cdx.json
sbom-convert convert sbom.cdx.json -f spdx-2.3

and lists spdx3, spdx-3.0.1 and cyclonedx-1.0 through cyclonedx-1.7 among the -f values.

Step 7: diff the result

bomctl has no diff command yet; it is on the roadmap. Until it ships, compare what matters with general purpose tools. One approach that does not depend on either project: export both the original and the converted or merged document to the same format, extract the list of package URLs and versions from each with a JSON query tool, sort, and compare the two lists with diff. Do the same for licences and for dependency edges. This is our suggestion, not a documented workflow from either project, and the exact query depends on which format you export to.

Look for missing components, duplicate PURLs, emptied or changed licence fields, lost dependency edges and dropped hashes.

Step 8 (for developers): read any SBOM in Go

If you are building a tool, the protobom README shows the core pattern: create a reader, parse a file, and walk the nodes. The same code reads SPDX or CycloneDX:

package main

import (
	"fmt"

	"github.com/protobom/protobom/pkg/reader"
)

func main() {
	// Create a new protobom SBOM reader:
	reader := reader.New()
	document, err := reader.ParseFile("sbom.spdx.json")
	if err != nil {
		fmt.Printf("ERROR: %v\n", err)
		return
	}

	// List all nodes in the SBOM and print some information about them:
	for _, node := range document.GetNodeList().GetNodes() {
		fmt.Printf("Node ID [%v]: name: %v version: %v\n", node.GetId(), node.GetName(), node.GetVersion())
	}
}

The README's third example goes the other way: build a document in code, relate file nodes to an application node with contains edges, and write SPDX 2.3 and CycloneDX 1.4 JSON from the same object.

Checking a converted SBOM against CISA's 2026 minimum elements

Because the 2026 guidance now names format and tooling fields explicitly, it is worth checking where each element lives in the neutral model and what survives conversion. This is our mapping from the schema, not an official one.

CISA 2026 elementWhere it lives in protobomWhat to check after conversion
SBOM AuthorMetadata.authorsAuthor is the entity that ran the tool, not the tool itself
SBOM Author SignatureNot in the model; signatures sit outside the documentOriginal bytes preserved if you need to verify a supplier signature
SBOM TimestampMetadata.dateA converted or merged document gets a new timestamp; record the source timestamp elsewhere if you need it
SBOM Data Format Name and VersionMetadata.source_data.format for the input; the target is set by the serializerThe output declares the new format; keep a record of the original
SBOM Generation ContextMetadata document types (SBOMType)SPDX 2.x has no native field; context can be lost going to 2.x
SBOM Tool Name and VersionMetadata.toolsConverters may add themselves as a tool; that is correct and expected
SBOM VersionMetadata.versionIncrement on every change
Component Name and VersionNode.name, Node.versionUnchanged in normal conversion
Component ProducerNode.suppliers, Node.originatorsSupplier versus originator mapping differs between formats
Component IdentifiersNode.identifiers (PURL, CPE 2.2, CPE 2.3, gitoid)SWHIDs and custom identifiers have no enum value
Component Hash Value and AlgorithmNode.hashesAlgorithms outside the target format's list may drop
Component LicenseNode.licenses, Node.license_concludedExpressions should stay expressions
Component Dependency RelationshipNodeList.edgesEdge types narrow when the target has fewer types
CoverageRoot elements, edges and external referencesLinked SBOMs must be reachable by the recipient

Governance, licence, cadence and how to contribute

Home. Both sit in the OpenSSF's SBOM tools area and meet through the Security Tooling Working Group (every other Friday, 8am Pacific); protobom also holds a community meeting every other Wednesday at 9am Pacific.

Governance. protobom's charter, adopted 12 April 2024, sets up a Technical Steering Committee whose voting members are initially the project's committers. Contributors become committers by majority approval of existing committers. bomctl, also a Series of LF Projects, has its own CHARTER.md, MAINTAINERS.md and an architecture decision record process.

Licence. Both repositories are Apache License 2.0, according to GitHub's licence detection.

Release cadence. protobom shipped seven releases between 20 April and 21 September 2026 (v0.5.5 to v0.6.2), and its repository was last pushed on 23 October 2026. bomctl is slower: its latest tagged release is v0.4.3 from 2 May 2025, and its main branch was last pushed on 20 November 2025. Check activity before you build a production dependency on bomctl, and consider the protobom library directly for new tooling.

Contributing. Both take GitHub pull requests under their CONTRIBUTING.md files and have OpenSSF Slack channels (#protobom, #bomctl). To add a protobom format, implement Serialize() and Render() for a writer or the Unserializer interface for a reader, one per format, version and encoding. bomctl's open decision records on linking and diff are where design input helps most.

Where Safeguard fits

Safeguard's SBOM generation and analysis are generally available. Safeguard does not use, ship or depend on protobom or bomctl, and we are not suggesting it replaces them; they solve a different problem. The fit is downstream: once you have a clean SPDX or CycloneDX document, merged or converted with whatever tools you prefer, something has to match its components to vulnerability data, show where each component is used and drive fixes.

What that looks like in practice, using only generally available capabilities:

  • Scan an SBOM file directly. The Safeguard CLI scans repositories (safeguard scan --dir .), container images (--image) and SBOM files (--sbom), and writes results as JSON under ./output. A document exported from bomctl or sbom-convert is just another SBOM file.
  • Generate your own SBOMs for repositories and images, so you have a first party document to merge with supplier ones.
  • Find where a component lives. The platform shows which projects contain a given component and version, which is the question people usually want a merged system SBOM to answer.
  • Match and rank. Vulnerability matching against vulnerability databases, with reachability analysis to rank the paths your code actually calls.
  • Gate releases. Policies and gates evaluated in the platform, so a build with a known vulnerable component can be stopped.

Where Safeguard does not help: it does not fix a supplier's low quality SBOM, and it does not replace verifying a supplier's SBOM signature with Sigstore tooling.

Common mistakes

Merging SBOMs without deduplicating identifiers. Two generators can describe the same package with different node IDs: one keyed by PURL, another by an internal SPDX ID. A merge that unions node lists without matching on PURL or hash produces duplicates, which inflate component counts, double vulnerability matches and confuse licence totals. bomctl's merge decision record names identifier matching as the goal and fuzzy matching as future work; protobom's newer Dedupe function exists in v0.6.2 but not in the protobom version bomctl pins. Check for duplicate PURLs after every merge.

Losing licence or relationship data on conversion. Licence expressions that turn into single IDs, file level licence information that disappears, and dependency scopes that collapse into plain dependencies are all documented, now fixed bugs in protobom's release history. Older embedded versions may still have them. Compare licence and edge counts before and after.

Assuming conversion fixes a low quality SBOM. A document with no versions, no PURLs, no hashes or no dependency graph is still that document after conversion. Fix it at the generator, and use CISA's 2026 guidance on explicitly marking unknown information so recipients can tell "unknown" from "missing".

Re-serializing a signed SBOM. If a supplier signed their SBOM, any re-serialization changes the bytes and breaks the signature. Keep the original, or use bomctl's original format when passing it on.

Trusting a README table over the release notes. protobom's support table still lists SPDX 3 as planned while v0.6.0 added SPDX 3.0.1. Check the code and the release you actually use.

Citing the NTIA 2021 minimum elements as current. bomctl's README still links them. CISA's 2026 Minimum Elements replaced them on 29 July 2026.

FAQ

What is protobom in simple terms?

protobom is a shared data model for SBOMs, written in protocol buffers, plus a Go library that reads SPDX and CycloneDX into that model and writes them back out. It lets a tool handle any supported SBOM through one set of types instead of separate parsers for each format.

Is protobom a new SBOM format?

No. Its design proposal calls a new SBOM format its most important non goal. You store and exchange SPDX or CycloneDX; protobom is the in memory representation a program uses while working with them.

Does protobom support SPDX 3?

Yes, from v0.6.0 (26 August 2026), which added full SPDX 3.0.1 support, with fixes to SPDX 3 output in v0.6.2. The README's support table still lists SPDX 3.0 as planned, which appears to be out of date. SPDX 3.1 is not yet supported.

Who created protobom?

According to the OpenSSF's 16 April 2024 announcement, CISA and DHS S&T's Silicon Valley Innovation Program funded a cohort of seven startups to develop it, and the OpenSSF hosts it. It is governed by a technical charter as a Series of LF Projects.

What is bomctl used for?

bomctl is a command line tool for fetching, importing, tagging, linking, merging, exporting and pushing SBOMs, built on protobom. It is aimed at systems made of many SBOMs, such as a container image plus the applications inside it.

Where does bomctl store SBOMs?

In a SQLite database file named bomctl.db, in a bomctl folder under your user cache directory by default. You can change the location with --cache-dir. Original document bytes are stored too, so unmodified documents can be exported exactly as received.

Can bomctl diff two SBOMs?

Not yet. diff is on the roadmap and its decision record is still marked proposed. Until then, export both documents to the same format and compare component, licence and edge lists with general purpose tools.

Does converting an SBOM between SPDX and CycloneDX lose data?

Some data can be lost. protobom aims to read without loss, but writing to a target format can drop fields that format lacks, and vulnerability, provenance and VEX data are out of its scope. Test round trips on your own documents.

A clean, merged, correctly converted SBOM is only useful if it leads to fixed software. The components it lists are open source dependencies and container images you build and ship, so the gap between finding a vulnerable version in the SBOM and running a fixed one in production is where the real work happens.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The first demo is a real Safeguard CLI session. You install the CLI, open the interactive terminal and ask it to fix a lodash CVE in a repository; it reads the finding and the advisory, asks before any command that changes something, edits package.json, runs the install and tests with your approval and pushes nothing. The same CLI's safeguard scan --sbom takes an SBOM file, so a document you merged with bomctl can go straight into it.

Click through it at your own pace, or open it full screen.

The second demo sets up the Safeguard runner on your own machine, approves it with a device code and then scans from the terminal. That is the shape of a pipeline step that fetches supplier SBOMs into a local bomctl cache, exports them, and scans the result on hardware you control.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which Safeguard tools Claude can use. Once connected, you can ask Claude about the projects, components and vulnerabilities in your Safeguard account in plain language.

Click through it at your own pace, or open it full screen.

To try it on your own SBOMs, download the Safeguard CLI and follow the CLI documentation.

Quick check

Your supplier signed their CycloneDX SBOM. You import it into bomctl and export it as CycloneDX again without changes. Will the signature still verify?

It should, if you export it in its original format and have not modified it in the cache: bomctl stores the original bytes and returns them in that case, and the original format forces it. Re-serializing it into a different format or after changes would break the signature.

A merged SBOM shows 1,240 components, but the two inputs had 700 and 600, and you know about 150 packages are shared. What probably went wrong?

The merge did not deduplicate shared packages. 700 plus 600 minus 150 is 1,150, so about 90 shared packages were kept twice, likely because the two generators used different node IDs. Check for duplicate PURLs and hashes.

Sources

Last reviewed: 25 October 2026. protobom releases frequently and bomctl is experimental; check the linked repositories for current versions, formats and command flags before you build on them.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.