OmniBOR is a draft specification for identifying software artifacts by their content and recording exactly which files went into each build. Every file gets an Artifact ID, a SHA-256 gitoid anyone can recompute, and every build output gets an Input Manifest listing the IDs of its inputs. Chained together, those manifests form an Artifact Dependency Graph.
This post goes one level below our explainer on OmniBOR IDs and SWHIDs inside a CycloneDX component: how a gitoid is computed byte by byte, what an Input Manifest looks like, how build tools are meant to embed it, what tooling exists, and what the graph can and cannot tell you. We read both the stable v0.1 specification and the v0.2 draft, the project documentation, the Rust implementation, the IANA gitoid registration, the CycloneDX, SPDX and SWHID specifications, and CISA's 2023 analysis. We also built a small C program and computed every identifier in the walkthrough ourselves; the hashes below come from that run.
Key takeaways
- An Artifact ID is a Git blob object ID computed with SHA-256: hash the ASCII string
blob, a space, the byte length in decimal, a NUL byte, then the file's bytes. It is written asgitoid:blob:sha256:followed by 64 lowercase hex characters. - The stable v0.1 specification lists both
gitoid:blob:sha1andgitoid:blob:sha256as mandatory types. The v0.2 draft on the specification's main branch, and the project's current documentation, permit SHA-256 only and add a rule that Windows line endings are normalized before hashing. - Input Manifests can reference their inputs' own manifests. When a build tool embeds the manifest ID inside the output, any change in the build chain changes the IDs of everything built from it.
- Toolchain support is prototype work: patches for GCC, LLVM, binutils and GNU patch, a strace based tracer (bomsh), and a Rust library and CLI. Stock GCC and GNU ld document no OmniBOR option.
- CycloneDX 1.6 has a
component.omniborIdarray. SPDX 3.0.1 has agitoidexternal identifier type and says artifact gitoids belong incontentIdentifierwhile manifest gitoids belong inexternalIdentifier. - A file's SWHID content hash equals its Git SHA-1 blob ID. Its SHA-256 OmniBOR ID matches neither.
- An Artifact Dependency Graph can show whether a specific source file's exact bytes went into a binary. It cannot show whether the vulnerable function is reachable, and it exists only if every build step recorded it.
What is OmniBOR, and what problem does it solve that a package-level SBOM does not?
Where the name came from
OmniBOR started life as GitBOM. The project's own resources page notes that its "Intro to GitBOM" slides "predate the project renaming from GitBOM to OmniBOR," and CISA's October 2023 report "Software Identification Ecosystem Option Analysis" refers to "OmniBOR (formerly GitBOM)." The IANA registration for the gitoid URI scheme still lists GitBOM as the scheme creator. We could not find a dated primary source that states the exact day of the rename, so we give no date here; what is certain is that the rename happened before October 2023.
A working group that meets weekly runs the project by consensus. The specification uses the Community Specification License 1.0 and the Rust code Apache 2.0.
The two concepts
The specification defines two things:
- Artifact Identifiers. A content based identifier for a single build input, usually one file. Two people holding the same bytes always derive the same identifier.
- Input Manifests. A list of the Artifact Identifier of every input used to produce an artifact. An executable's manifest lists each object file's ID; each object file's manifest lists the source and header files compiled into it.
An artifact is any object that can be represented as an array of bytes: source files, .o objects, .jar files, executables, container images. A build tool is anything that reads input artifacts and writes output artifacts: compilers, linkers, runtimes and code generators such as patch.
Why a package-level SBOM is not enough
A typical SBOM is a list of packages, each named by a Package URL or a CPE. That is the right granularity for most vulnerability matching. It has three gaps that OmniBOR targets.
First, a package-level SBOM is an assertion. Nothing in a purl is derived from the bytes you shipped, so a backported fix or a patched vendored copy is invisible.
Second, it stops at the package boundary. Compile time flags decide which of a library's source files reach the binary, and an advisory often names a single file.
Third, the identifiers need a central authority. The OmniBOR documentation makes this point directly: CPE relies on a dictionary maintained by NIST, and Package URLs rely on a known list of package types. OmniBOR calls its identifiers inherent: anyone holding the artifact can compute its ID without asking anyone.
CISA's 2023 analysis draws the same line between defined identifiers such as CPE, assigned by certain parties, and inherent identifiers anyone can derive from the software. It concludes that OmniBOR is a strong starting point for inherent identifiers, with caveats we cover below.
What is a gitoid, and how is it computed?
The Git object construction
Git stores every file, directory listing, commit and tag as an object, and names each object by a hash. Git's internals documentation explains the construction: Git builds a header from the object type, a space, the content length in bytes and a NUL byte, prepends it to the content, and hashes the result. The IANA registration for the gitoid URI scheme gives the same header in its formal syntax and defines the URI form:
gitoid:<git object type>:<hash algorithm>:<hash value>
The provisional IANA entry allows four object types (blob, tree, commit, tag) and two hash algorithms (sha1, sha256), in lowercase hex.
OmniBOR uses only the blob type, so the header is always blob, a space, the decimal length, and a NUL byte. The documentation notes that hashing the length in also makes collision attacks harder.
Worked example 1: a one line text file
Take a file containing hello world followed by a line feed: 11 characters plus one byte, 12 bytes in all. The OmniBOR construction hashes the 4 bytes blob, one space, the two digits 12, one NUL byte, and then the 12 content bytes: 20 bytes in total.
| Construction | Hash input | Result |
|---|---|---|
| Plain SHA-256 of the file | 12 content bytes | a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 |
Git SHA-1 blob ID (git hash-object) | 8 header bytes plus 12 content bytes | 3b18e512dba79e4c8300dd08aeb37f8e728b8dad |
| OmniBOR Artifact ID (SHA-256 gitoid) | 8 header bytes plus 12 content bytes | gitoid:blob:sha256:0bd69098bd9b9cc5934a610ab65da429b525361147faa7b5b922919e9a23143d |
Three numbers, all for the same file. The plain SHA-256 differs from the OmniBOR ID because the header is not included. The SHA-1 blob ID is what you see in any ordinary Git repository. We confirmed the SHA-256 value with a Python function and with git hash-object in a SHA-256 repository.
Worked example 2: the same text with Windows line endings
Now save the same text with a carriage return before the line feed: 13 bytes, hello world\r\n. A raw SHA-256 gitoid of those 13 bytes is 7aa67785e5e5377ce9906a59442e80a6efe083563ef32648debda5ed99da274a, because the header now says 13 and the content has an extra byte.
The v0.2 draft specification adds a rule here. Section 6.1.2 requires that every 0D 0A byte pair (a Windows line ending) be converted to 0A before hashing, whatever the artifact is. Under that rule, the Windows line ending file and the Unix line ending file get the same Artifact ID, 0bd69098...143d. Git, by contrast, hashes whatever bytes it is given unless a line ending filter is configured.
So under the v0.2 draft, an OmniBOR ID equals the SHA-256 Git blob ID only when the file, text or binary, contains no 0D 0A pair. The stable v0.1 specification has no normalization rule, so state which version your code follows and check what your library does.
SHA-1, SHA-1CD and SHA-256
The stable v0.1 specification says "mandatory identifier types" means both gitoid:blob:sha1 and gitoid:blob:sha256, and that every Input Manifest uses exactly one of them. The v0.2 draft and the project's Artifact IDs documentation narrow that to SHA-256 only. The documentation gives three reasons. First, after the SHAttered collision attack Git switched to SHA-1CD, a collision detecting variant that changes its output when it detects an attack, while still calling it SHA-1; a universal identifier has to say which function it means, so supporting SHA-1 would really mean supporting two. Second, Git itself is moving to SHA-256: its hash function transition document records that the project "picked SHA-256 as its successor hash" in 2018 and defines a repository format with extensions.objectFormat = sha256. Third, the project expects governments to require moves away from SHA-1, as happened with MD5. The specification reserves the right to add algorithms if SHA-256 is ever broken.
For you, the result is simple. New OmniBOR data should use gitoid:blob:sha256. If you meet older tooling or data that uses gitoid:blob:sha1, it was valid under v0.1, and the SHA-1 value is the familiar Git blob ID.
Why the ID is reproducible across machines
A gitoid depends only on the bytes. It does not include a file name, a path, a timestamp, permissions, an owner or a repository. That is what lets two parties who have never communicated agree on an identifier, and it is why the specification treats two artifacts as equivalent only when their bytes are identical.
The catch: the ID is reproducible for the same bytes, not for the same build. A compiler that writes a timestamp or absolute path into an object file gives every build a new ID. In our test, compiling greet.c twice with Apple clang produced identical objects (9696e119...3bce), but that is a property of that compiler and those flags. Reproducible build practices keep derived IDs stable.
What is an Artifact Dependency Graph, and how is an Input Manifest built?
The Input Manifest format
An Input Manifest is a small ASCII text file. The v0.2 draft defines it precisely.
The first line is a header naming the identifier type, ending in a line feed:
gitoid:blob:sha256
Every following line is one input record. A record is either the input's Artifact ID in lowercase hex, or the input's Artifact ID followed by a space, the word manifest, a space, and the Artifact ID of that input's own Input Manifest. The specification's example:
gitoid:blob:sha256
09c825ac02df9150e4f93d12ba1da5d1ff5846c3e62503c814aa3a300c535772
230f3515d1306690815bd9c3da0d15d8b6fcf43894d17100eb44b6d329a92f61
2f4a51b16b76bbc87c4c27af8ae062b1b50b280f1ab78e3eec155334588dc88e manifest 4f3a822f776412c049dda53c3277bf2225b51b805ce8a99222af23a7d9f55636
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4
f47ffb3518f236eea6525fd29f057ddd5cda1bb803ccc662e6bc5925afd1e4af
Four rules make the manifest itself reproducible:
- Records are sorted in lexical order.
- Every identifier in a manifest uses the type declared in the header. A manifest never mixes SHA-1 and SHA-256.
- Line endings are always a single
\n, never the platform's line delimiter. - The whole file is ASCII.
Because a manifest is itself an array of bytes, it has its own Artifact ID, computed with the same gitoid construction. That ID is how other manifests point to it.
A manifest contains no file names, paths, flags, timestamps or tool identity. It records which exact inputs were used, nothing about how, which is the main difference from richer build records such as SLSA provenance.
Where manifests are stored
Section 7 of the v0.2 draft defines a file system layout. If a build tool writes manifests locally, it writes them under:
${OMNIBOR_DIR}/manifests/gitoid_blob_sha256/<first 2 hex chars>/<remaining 62 hex chars>
The colons in the type prefix become underscores, and the first two hex characters of the manifest's own ID form a fan out directory, the same trick Git uses in .git/objects. The storage location comes from the OMNIBOR_DIR environment variable, overridden by a tool specific flag. If neither is set, a tool may skip manifest generation, so OmniBOR output is opt in.
How an output carries a link to its inputs
A manifest stored on disk is useful, but it can be separated from the binary it describes. The specification's key move is embedding. Section 6.2.4 says each build tool should embed the Artifact ID of its Input Manifest into the output artifact, in a manner appropriate to and generally agreed for that file format, and should skip embedding if the format cannot carry extra data without breaking.
The v0.2 draft defines two embeddings.
ELF files. A build tool writes a section named .note.omnibor of type SHT_NOTE with the SHF_ALLOC flag. Each note entry has a 4 byte namesz set to 8, a 4 byte descz (the specification's spelling) giving the descriptor length including a terminating NUL, a 4 byte type (NT_GITOID_BLOB_SHA256 is 0x1), the 8 byte owner string OMNIBOR plus a NUL, and a descriptor carrying the manifest's Artifact ID. There is one note per identifier type, in ascending type order.
Generated source files. A tool that writes source code (the specification's examples are patch and Go's stringer) appends a comment as the last line, preceded by a blank line:
// OmniBOR-Input-Manifests: [ gitoid:blob:sha256:09c825ac02df9150e4f93d12ba1da5d1ff5846c3e62503c814aa3a300c535772 ]
Shell style languages use # instead. A reader takes the last such line in the file. The draft spells the marker both OmniBOR-Input-Manifests and OmniBOR-Input-Manifest in different places, so a parser should accept both.
From manifests to a graph
Once the manifest ID is embedded in the output, the output's own Artifact ID depends on the manifest. Change any input, and the manifest changes, so its ID changes, so the embedded bytes change, so the output's ID changes. That output is the next step's input, so the change propagates all the way to the final executable or container image. The specification says Artifact IDs and Input Manifests together form "a kind of Merkle Tree."
The Artifact Dependency Graph is what you get by following those links. The glossary defines it as the directed acyclic graph of every artifact transformed into the final one, "all the way down to source code." Leaf artifacts are hand written sources; derived artifacts are everything a build tool wrote.
For a simple C program the graph looks like this:
| Level | Artifact | Derived from | Record in parent manifest |
|---|---|---|---|
| 0 (leaf) | greet.h, greet.c, main.c | Written by a person | Plain Artifact ID |
| 1 | greet.o | greet.c and greet.h via the compiler | Artifact ID plus manifest and the ID of greet.o's manifest |
| 1 | main.o | main.c and greet.h via the compiler | Artifact ID plus manifest and the ID of main.o's manifest |
| 2 | hello | greet.o and main.o via the linker | This artifact's own manifest is the root |
Headers count: greet.h appears in both object files' manifests because both translation units included it. And the graph covers build inputs, not runtime ones; shared libraries loaded by the dynamic linker need a runtime tool to record.
How do compilers and linkers emit OmniBOR data today?
This is the question where marketing tends to run ahead of reality, so we checked carefully.
Build tool patches
The OmniBOR project page lists patched versions of GCC (gcc-omnibor), binutils (binutils-omnibor), LLVM (llvm-omnibor) and GNU patch (patch-omnibor). Its third party integrations page states the long term plan: get manifest generation accepted upstream in compilers, linkers, archivers, bundlers and container tools. The GitHub repository descriptions call the GCC, LLVM and binutils work "prototyping support." GitHub shows their last pushes in November 2023 (GCC), January 2024 (LLVM) and February 2024 (binutils).
We also searched the current GCC option index and the GNU ld options manual for any OmniBOR option and found none. As of the date of this post, then, you should treat compiler and linker emission as available only through patched toolchains you build yourself, not through a flag in your distribution's stock compiler. The project's documentation agrees: wrapper tooling for unmodified tools "is not yet ready."
bomsh: tracing a build from the outside
Because patched compilers are hard to deploy, the project's bomsh repository takes the opposite approach. It watches a build from outside and reconstructs the graph. Its tools are:
- Bomtrace2 and Bomtrace3, patched versions of
stracethat record every process the build runs, with the files each one reads and writes. The README says Bombash and the original Bomtrace are deprecated. Bomtrace2 runs 2x to 5x slower than a normal build; Bomtrace3 moved its hook logic into C and adds "only about 20% runtime overhead." - bomsh_create_bom.py, which turns the raw trace log into OmniBOR documents in an output directory.
- Rebuild scripts for Debian and RPM packages (
bomsh_rebuild_deb.py,bomsh_rebuild_rpm.py) that reproducibly rebuild a package from its buildinfo or source RPM and generate its OmniBOR data, optionally with SPDX documents that carry an OmniBOR external reference. - CVE helpers (
bomsh_create_cve.py,bomsh_search_cve.py) that build a database from a Git repository and search an artifact tree for files associated with known CVEs.
The README's Bomtrace3 flow is: run your build under it (bomtrace3 make), then run bomsh_create_bom.py on the raw log. Tracing works with unmodified compilers on Linux, but it records without embedding, so binaries do not carry their manifest IDs.
The Rust implementation and CLI
The omnibor-rs repository holds two crates. The omnibor library implements identifiers and manifests, and the omnibor-cli crate is a command line tool for scripts where linking the library through its C foreign function interface is not worth the effort. Both are Apache 2.0. On crates.io, omnibor-cli was first published in March 2024 and its newest version, 0.9.3, was published on 18 June 2025.
The CLI README shows omnibor id create <file> printing an Artifact ID in plain, JSON or short format, and omnibor id find <id> <path> searching a directory tree for a file with a given ID. The README says the CLI "only supports working with identifiers, not manifests" and pins an older v0.6.0 installer. The source on the main branch has moved on: it defines id, manifest, store and debug subcommands, with manifest create taking inputs, a --target and a --store flag. Run omnibor --help on the version you install. We did not have a Rust toolchain on our test machine, so the walkthrough below computes IDs with Git and a short script and shows the CLI commands as documented rather than as run. The Go, .NET and Python implementations were last pushed in 2023 and describe themselves as experimental or proof of concept.
Summary of integration options
| Approach | What it records | Embeds manifest ID in output | Works with stock tools | Maturity on 24 Oct 2026 |
|---|---|---|---|---|
| Patched GCC, LLVM, binutils | Inputs seen by each compiler and linker invocation | Yes, by design (ELF note) | No, needs a patched toolchain | Prototype, last pushed 2023 to early 2024 |
| bomsh Bomtrace3 | Every file read and written by each traced process | No | Yes, on Linux | Most complete project tool; scripts for Debian and RPM rebuilds |
omnibor Rust crate | Whatever your own build integration feeds it | Up to your integration | Yes, as a library | Reference implementation, versioned on crates.io |
omnibor-cli | IDs for files and directories; manifests on newer builds | No | Yes | 0.9.3 on crates.io; README lags the source |
How do OmniBOR IDs appear in CycloneDX and SPDX 3?
CycloneDX 1.6: omniborId
CycloneDX 1.6 adds an omniborId property to component. It is an array of strings, and the schema description says it "asserts the identity of the component using the OmniBOR Artifact ID" and must conform to the IANA gitoid URI definition. The same release adds a parallel swhid array. Both descriptions point to evidence.identity as the place to record evidence that substantiates the asserted identity.
The field is an array, so a component can carry several IDs, for example an older v0.1 SHA-1 ID alongside a SHA-256 one. It identifies the component, not its build inputs: CycloneDX has no field for the Artifact ID of a component's Input Manifest.
A minimal component with a purl, a hash and an OmniBOR ID looks like this. The values are from our walkthrough build:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": [
{
"type": "application",
"name": "hello",
"version": "0.1.0",
"purl": "pkg:generic/hello@0.1.0",
"hashes": [
{ "alg": "SHA-256", "content": "a37a58bef2327df0544f2d475723cb16eac487e8e696d0115c1949409ad0210e" }
],
"omniborId": [
"gitoid:blob:sha256:150e767b1364a6deb728752e43ae0735498b9a7d5a8c5b4ee7f301080c083b19"
]
}
]
}
Note that hashes and omniborId are different numbers for the same binary. The hash is a plain SHA-256 of the file. The OmniBOR ID includes the Git blob header. A consumer comparing them directly will conclude they disagree.
SPDX 3.0.1: gitoid external identifiers and content identifiers
SPDX 3.0.1 includes gitoid in its ExternalIdentifierType vocabulary, defined as "Git Object ID." The definition notes that a gitoid can identify either an artifact or its Input Manifest, because the manifest is itself an artifact. SPDX resolves it by placement:
- Gitoids calculated on software artifacts (Snippet, File or Package elements) should be recorded in the SoftwareArtifact's
contentIdentifierproperty. - Gitoids calculated on the Input Manifest should be recorded in the element's
externalIdentifierproperty.
So an SPDX 3 document can carry both the artifact's ID and a pointer to its manifest without inventing a convention. The same vocabulary includes swhid, packageUrl, cpe22 and cpe23.
How OmniBOR relates to SWHID
The SWHID specification defines content identifiers (swh:1:cnt:) as the SHA-1 of the ASCII string blob, a space, the length in decimal, a NUL byte and the content: the same construction as a Git SHA-1 blob ID. For our one line file, the SWHID would therefore be swh:1:cnt:3b18e512dba79e4c8300dd08aeb37f8e728b8dad, the same hex value as the Git SHA-1 blob ID and the v0.1 style gitoid:blob:sha1 ID.
The SHA-256 OmniBOR ID, 0bd69098..., shares nothing with it except the construction. So the relationship is: same family, different hash. SWHID also covers directories, revisions, releases and snapshots; OmniBOR adds the manifest layer for build inputs. The SWHID project reports that SWHID was adopted as ISO/IEC 18670:2025 on 23 April 2025. The OmniBOR specification remains a community draft.
How OmniBOR relates to purl and CPE
Package URL describes where a package comes from: type, namespace, name, version, qualifiers and subpath, as in pkg:npm/lodash@4.17.21. CPE describes a product by vendor, product and version, from a dictionary NIST maintains. Both are names that a person or a registry assigns. Neither is derived from bytes, and both are what vulnerability databases key their records on today. OmniBOR's own documentation calls its identifiers "complementary" to purl and CPE, and CISA's report makes the same point from the other side: inherent identifiers give precision, defined identifiers give grouping, and no single scheme does both.
Comparison: purl, CPE, SWHID, OmniBOR ID and a plain SHA-256
| Identifier | What it identifies | Who can compute or assign it | Derived from content | Where it is used | Groups versions or ranges |
|---|---|---|---|---|---|
| Package URL (purl) | A package in a named ecosystem, by type, name and version | Anyone who knows the package coordinates; meaning depends on the ecosystem's registry | No | SBOMs, vulnerability databases, package manager tooling | Yes, through version ranges in advisories |
| CPE 2.3 | A product or platform by vendor, product and version | Assigned through the NIST CPE dictionary | No | NVD and many vulnerability records | Yes, with wildcards and version ranges |
SWHID (swh:1:cnt: and others) | A file, directory, commit, release or snapshot | Anyone with the object; SHA-1 based | Yes | Software Heritage archive, research, provenance, SPDX 3 and CycloneDX 1.6 fields | No |
OmniBOR Artifact ID (gitoid:blob:sha256:) | Any array of bytes: a source file, object file, binary or image | Anyone with the bytes; SHA-256 with Git blob header, CRLF normalized under the v0.2 draft | Yes | OmniBOR manifests, CycloneDX omniborId, SPDX 3 contentIdentifier | No |
| OmniBOR Input Manifest ID | The exact set of inputs a build tool consumed | The build tool that ran the build, or a tracer that watched it | Yes, from the manifest text | Embedded in ELF notes and generated sources; SPDX 3 externalIdentifier | No |
| Plain SHA-256 | Any array of bytes | Anyone with the bytes | Yes | Package lockfiles, container digests, SBOM hashes, signatures | No |
Inherent identifiers answer "is this exactly the thing?" Defined identifiers answer "is this a version of the thing an advisory names?" Vulnerability response needs both.
What can an Artifact Dependency Graph answer during vulnerability response?
The question it answers well
The specification says the graph can supplement vulnerability information by identifying affected files precisely. The Rust README adds that file level matching sidesteps the problem of comparing version numbers across platforms and patching practices.
Concretely, suppose an advisory's fix commit changes src/parser.c. You can compute the Artifact ID of the vulnerable version of that file and the fixed version. With a complete graph for your shipped binary, you can walk down from the binary's manifest and answer, without guessing:
- Was the vulnerable
parser.ccompiled into this binary? If its ID appears in any manifest in the graph, yes. - Was the fixed version compiled in instead? If the fixed file's ID appears, the backport landed, whatever the version string says.
- Was neither compiled in? Then the file was excluded by build configuration, or the graph is incomplete, and you need to tell those apart.
bomsh's CVE scripts implement a version of this today.
The limits
A file is not a function. Knowing that the vulnerable file was compiled in tells you the vulnerable code is present. It does not tell you whether your program can reach the vulnerable function, whether the linker discarded it as dead code, or whether link time optimization inlined it somewhere else. Reachability needs a different analysis.
The graph only exists if every step recorded it. One unpatched tool in the chain, such as the archiver that builds a static library, breaks the link, and vendor binaries arrive with no manifest at all.
Mapping vulnerabilities to file IDs is extra work. Advisories list purls, CPEs and version ranges, not file IDs. Someone has to compute IDs from fix commits for every affected version, and that data does not exist at scale.
IDs churn without reproducible builds. Source IDs are stable; derived IDs change with every embedded timestamp, which complicates comparison across builds.
Bundled ecosystems fit awkwardly. A JavaScript bundler that concatenates hundreds of modules, or an image builder stacking layers, needs its own OmniBOR support to record what it consumed.
Manifest distribution and privacy. The graph is only useful to a consumer who receives all the manifests. Manifests list IDs, not names, but anyone holding a candidate source file can confirm whether it was used. The specification defines local storage, not a distribution protocol.
CISA's caveat: files are not products. CISA's analysis points out that OmniBOR identifies individual files but not necessarily whole applications, because the standard does not normalize which files count as a product's inputs. Name, vendor and version are not inherent properties, so OmniBOR IDs alone cannot express "all versions before 2.4.1." That is why purl and CPE are not going away.
Hash strength is a dependency. Under the v0.2 draft everything rests on SHA-256. The specification reserves the right to add algorithms. Manifests would then need regenerating, since one manifest never mixes types.
Decision table: when to add OmniBOR to your pipeline
| Your situation | Is OmniBOR worth it now? | What to do |
|---|---|---|
| You ship compiled C or C++ and need to answer "is this file in our binary" for advisories | Yes, as an experiment | Trace builds with Bomtrace3 on Linux, store manifests, record artifact IDs in SPDX 3 or CycloneDX |
| You build distribution packages and backport fixes | Yes | Generate manifests during rebuilds; file IDs show backports that version strings hide |
| You consume third party binaries only | Limited | Ask suppliers whether they publish Artifact IDs and manifests; record the IDs of what you receive |
| Your stack is mostly npm, PyPI or Maven packages pulled from registries | Low priority | Keep purl based SBOMs and lockfile hashes; OmniBOR adds little until bundlers and package tools support it |
| You need an identifier an auditor or regulator recognizes today | Not as the primary ID | Use purl and CPE in the SBOM; add omniborId as supporting evidence |
| You are building internal provenance tooling | Yes | The manifest format is small and well defined; the Rust crate is the reference implementation |
Step by step: compute gitoids, build a small C project with OmniBOR output, and add the IDs to a CycloneDX SBOM
This walkthrough runs on Linux or macOS. We ran it on macOS with Apple clang 21 and Git 2.54. Because stock compilers do not emit OmniBOR data, steps 4 and 5 build the manifests from the outside with a short script that follows the v0.2 draft. On Linux, you can replace steps 4 and 5 with Bomtrace3 as described in the bomsh README.
Step 1: install the OmniBOR CLI
The CLI README offers three routes. If you have a Rust toolchain, install from crates.io:
cargo install omnibor-cli
omnibor --version
omnibor --help
With cargo-binstall you can fetch a prebuilt binary instead:
cargo binstall omnibor-cli
The README also offers a shell installer from the project's GitHub releases. If you use it, download the script, read it, and pin the release tag rather than piping a URL straight into a shell. The README's example pins v0.6.0, which is older than the 0.9.3 release on crates.io, so pick the tag deliberately.
Step 2: compute gitoids for files
With the CLI installed, the documented commands are:
omnibor id create Cargo.toml
omnibor id create Cargo.toml -f json
omnibor id create src/
The first prints a line of the form Cargo.toml => gitoid:blob:sha256:.... With -f json you get an object with id and path. Given a directory, the source says it recursively prints IDs for every file under it.
You can check the CLI against Git. Create a SHA-256 repository and hash a file inside it:
printf 'hello world\n' > hello.txt
git init -q --object-format=sha256 /tmp/oid256
git -C /tmp/oid256 hash-object "$PWD/hello.txt"
# 0bd69098bd9b9cc5934a610ab65da429b525361147faa7b5b922919e9a23143d
git hash-object hello.txt
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad (SHA-1 blob ID, equal to the SWHID content hash)
The SHA-256 value should equal the CLI's output for any file without Windows line endings. For files that contain 0D 0A byte pairs, a tool following the v0.2 draft normalizes first, so the values will differ from Git's.
Without the CLI, tools/gitoid.py below does the same: it is the gid function from the Step 4 script with a loop that prints path => gitoid:blob:sha256:... for each argument.
Step 3: build a small C project
Create three files:
/* greet.h */
#ifndef GREET_H
#define GREET_H
void greet(const char *name);
#endif
/* greet.c */
#include <stdio.h>
#include "greet.h"
void greet(const char *name) { printf("hello, %s\n", name); }
/* main.c */
#include "greet.h"
int main(void) { greet("omnibor"); return 0; }
Compile each translation unit, link, and ask the compiler which headers each object depends on:
cc -c greet.c -o greet.o
cc -c main.c -o main.o
cc main.o greet.o -o hello
./hello
cc -MM main.c greet.c
The -MM output lists main.c greet.h for main.o and greet.c greet.h for greet.o. On macOS it also listed the SDK's SDKSettings.json: real builds read files you did not write, and a tracer will record them. Then hash everything:
python3 tools/gitoid.py greet.h greet.c main.c greet.o main.o hello
Our run produced:
| File | Artifact ID (SHA-256 gitoid, first and last 4 hex) |
|---|---|
greet.h | 75d7...bba4 |
greet.c | 3760...6ef3 |
main.c | ca07...7f0b |
greet.o | 9696...3bce |
main.o | d451...03d9 |
hello | 150e...3b19 |
Your object file and binary IDs will differ from ours; compilers, flags and platforms all change the bytes. The source file IDs will match ours if you copy the files exactly.
Step 4: write the Input Manifests
For each derived artifact, list its inputs, sort the records, add the header, hash the result, and store it under OMNIBOR_DIR. This script does exactly that:
# manifest.py usage: manifest.py TARGET INPUT[=INPUT_MANIFEST_ID] ...
import hashlib, os, sys
def gid(data: bytes) -> str:
data = data.replace(b"\r\n", b"\n")
return hashlib.sha256(b"blob %d\0" % len(data) + data).hexdigest()
target, records = sys.argv[1], []
for arg in sys.argv[2:]:
path, _, mid = arg.partition("=")
aid = gid(open(path, "rb").read())
records.append(f"{aid} manifest {mid}" if mid else aid)
body = "gitoid:blob:sha256\n" + "".join(r + "\n" for r in sorted(records))
mid = gid(body.encode("ascii"))
root = os.environ.get("OMNIBOR_DIR", ".omnibor")
d = os.path.join(root, "manifests", "gitoid_blob_sha256", mid[:2])
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, mid[2:]), "w", newline="\n") as f:
f.write(body)
print(f"{target} manifest => gitoid:blob:sha256:{mid}")
Run it bottom up, feeding each object file's manifest ID into the linker step:
export OMNIBOR_DIR=.omnibor
G=$(python3 tools/manifest.py greet.o greet.c greet.h | awk -F'sha256:' '{print $2}')
M=$(python3 tools/manifest.py main.o main.c greet.h | awk -F'sha256:' '{print $2}')
python3 tools/manifest.py hello "greet.o=$G" "main.o=$M"
find .omnibor -type f | sort
Our run wrote three manifests. The one for hello, with ID gitoid:blob:sha256:0e6af8ba7837ae5a94144ebcc466a99134d67372b6def77eb8b81329e408cf16, reads:
gitoid:blob:sha256
9696e1199fe24fc60220172986bc507878db654954ae56bba175d5e68f6c3bce manifest 8de86c41989a71e8e808f9b059670ab467863e667da1f08d2d8c9458a470dff9
d451f41083c3966c60ea2089165a5bdd1148185218c7b597b0cb3172ed7703d9 manifest 751098ea9f672dc59845c8bf0ef1c33f7c65c767e6926e3bcdf685ad17cb987c
The greet.o manifest (8de8...dff9) lists the IDs of greet.c and greet.h. The main.o manifest (7510...987c) lists greet.h and main.c. Sorting puts 75d7... (greet.h) before ca07... (main.c), regardless of file name, because the records are sorted by ID.
On a newer omnibor-cli build, the source defines an equivalent command, omnibor manifest create --target greet.o greet.c greet.h --store. Check omnibor manifest create --help first; the flags may differ in the release you install.
Step 5: walk the graph to answer a question
Suppose an advisory names greet.c at the exact content we compiled. Its ID is 376005bc...6ef3. To check whether it went into hello:
grep -rl 376005bc19e83653d1a5c367e50a9bb685a64a6c9a9dda29b0e03f5c5a2a6ef3 .omnibor/manifests
The match is the greet.o manifest. The hello manifest references that manifest ID, so the file is in the graph for hello. A real tool does this recursively from the root manifest rather than with grep, and it has to handle manifests it cannot find.
Note the limit of this walkthrough. Because we did not embed the manifest ID into hello (Mach-O on macOS, and no patched linker), nothing inside the binary points to 0e6af8ba.... The link from binary to manifest exists only because we kept them together. On Linux with a patched toolchain, the .note.omnibor section would carry it, and readelf --notes hello would show a note owned by OMNIBOR.
Step 6: add the IDs to a CycloneDX SBOM
Add the binary's Artifact ID to the component's omniborId array, as in the JSON example above. If your SBOM lists source files as components (some file level SBOMs do), add each file's ID to its component. Then validate the document against the CycloneDX 1.6 schema with whatever validator you already use, and check that every omniborId value matches the gitoid:blob:sha256: form with 64 lowercase hex characters.
CycloneDX has no dedicated field for the manifest, so use an externalReferences entry of type other pointing to where it can be fetched, or a namespaced property, and document the convention. In SPDX 3, use contentIdentifier for the artifact's gitoid and externalIdentifier of type gitoid for the manifest's.
Common mistakes
- Comparing an OmniBOR ID with a plain SHA-256. They are different values for the same file, because the gitoid includes the
blobheader. Ahashesentry and anomniborIdentry in the same component should not match. - Assuming the SHA-1 Git blob ID is an OmniBOR ID. Valid under v0.1, not under the v0.2 draft.
- Forgetting line ending normalization. Under the v0.2 draft a Windows line ending file's ID differs from Git's SHA-256 blob ID. Test your library.
- Writing manifests with platform line endings. A manifest written with
\r\nhashes differently. Always write\n. - Sorting records by file name. Records sort by ID, not path.
- Leaving out headers and generated files. Headers are inputs to every object that includes them; generated sources need their own manifests.
- Treating "file present" as "vulnerable and exploitable." An ADG shows inclusion. It does not show reachability, dead code elimination or configuration.
- Assuming stock compilers emit OmniBOR. As of this writing they do not. Ask for the patch set and version when a supplier claims support.
FAQ
What does OmniBOR stand for?
The project's own pages do not expand the acronym; CISA's 2023 report glosses it as Universal Bill of Receipts. The project describes itself as "reproducible identifiers and fine-grained build dependency tracking for software artifacts."
Is OmniBOR the same as GitBOM?
Yes. GitBOM was the earlier name; the project's resources page and CISA's October 2023 report both record the rename.
Is an OmniBOR ID the same as a Git commit hash?
No. A commit hash identifies a commit object, which records a tree, parents, author and message. An OmniBOR Artifact ID identifies a single file's bytes as a Git blob object, computed with SHA-256. If your repository uses SHA-256 objects, the blob ID Git stores for a file with Unix line endings equals that file's OmniBOR ID.
Does OmniBOR replace SBOMs?
No. It adds file level identity and build inputs underneath an SBOM. CycloneDX 1.6 and SPDX 3.0.1 both have places to record OmniBOR IDs inside an SBOM, and package identifiers such as purl remain what vulnerability databases match on.
What is the difference between OmniBOR and SWHID?
Both hash content with Git's blob construction. SWHID uses SHA-1, covers directories, commits, releases and snapshots, and is ISO/IEC 18670:2025. OmniBOR uses SHA-256 for files only and adds Input Manifests that record build inputs. A file's SWHID content hash equals its Git SHA-1 blob ID, not its OmniBOR ID.
Does GCC or LLVM support OmniBOR?
Not in the stock releases we checked. The OmniBOR project maintains prototype patches for GCC, LLVM and binutils, last pushed between November 2023 and February 2024. The current GCC option index and GNU ld manual list no OmniBOR option.
Can OmniBOR tell me whether my binary is vulnerable to a CVE?
It can tell you whether a specific vulnerable source file, by exact content, was compiled into the binary, provided you have a complete graph and someone has mapped the CVE to file IDs. It cannot tell you whether the vulnerable code is reachable or exploitable.
Where do OmniBOR IDs go in an SPDX 3 document?
The artifact's own gitoid goes in contentIdentifier on the File, Package or Snippet. The Input Manifest's gitoid goes in externalIdentifier with type gitoid. That is the split SPDX 3.0.1's vocabulary specifies.
Where Safeguard fits
OmniBOR Artifact IDs, Input Manifests and Artifact Dependency Graphs come from your build tooling; the Safeguard features below work on the SBOM and dependency side of the same problem. What it does with SBOMs is package level: it generates SBOMs in SPDX and CycloneDX formats, it reads SBOM files you already have (safeguard scan --sbom <file>), and it matches the components it finds against vulnerability databases, with reachability analysis to rank which vulnerable code paths your application actually calls. That is the complement to an ADG described in the limits section above: the graph tells you a vulnerable file was compiled in, and reachability helps you judge whether it matters. The fit is with the package level layer and the code and containers around your build, not with gitoid computation or manifest storage.
OmniBOR exists because vulnerable code reaches binaries through build inputs that nobody tracked, and the same inputs arrive through the package managers and base images your build pulls every day. Whether or not you adopt manifests, closing the gap between a published fix and your next release is still the part that reduces risk, and that is where the steps below apply.
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 covers eight categories, including dependencies, licenses, security posture and findings. 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. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature 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. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull 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 demos show Safeguard on ordinary software projects. None computes gitoids or builds manifests; they show the package level scanning, fixing and approval flow that sits alongside OmniBOR data.
The first demo is a real Safeguard CLI session. After install, safeguard opens the interactive terminal, you ask it to fix the lodash CVE in a repository, and it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. It ends with /permissions, /model and /scan. The same loop fits a repository holding your build scripts and SBOM tooling.
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 its device code, and then scans from the terminal. On a build host you would scan with safeguard scan --dir ., --image for a container image, or --sbom for an SBOM you already produced, with JSON results under ./output.
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 lets you choose which tools Claude can use. If Claude helps you read SBOMs or build logs, this is how it gets Safeguard scan results, with you choosing which tools it may call.
Click through it at your own pace, or open it full screen.
Download Safeguard at safeguard.sh/download and read the CLI documentation. For build records alongside attestations and signatures, see CycloneDX attestations vs in-toto, SLSA provenance and Sigstore bundles and the six SBOM types.
Quick check
A file contains "hello world" plus a line feed, 12 bytes in all. What exact bytes are hashed to produce its OmniBOR Artifact ID?
The 7 byte string blob 12 and a NUL byte (8 header bytes), then the 12 content bytes, hashed with SHA-256. The result is gitoid:blob:sha256:0bd69098bd9b9cc5934a610ab65da429b525361147faa7b5b922919e9a23143d, which differs from the plain SHA-256 of the file because the header is included.
In SPDX 3.0.1, where does the gitoid of a file's Input Manifest go, as opposed to the gitoid of the file itself?
The manifest's gitoid goes in the element's externalIdentifier property with type gitoid. The file's own gitoid goes in the SoftwareArtifact's contentIdentifier property.
Sources
- OmniBOR project home
- OmniBOR Specification, Version 0.1 (stable, project website)
- OmniBOR Specification, Version 0.2 draft (spec/SPEC.md, main branch)
- OmniBOR specification repository README
- OmniBOR documentation: Artifact IDs
- OmniBOR documentation: Input Manifests
- OmniBOR glossary
- OmniBOR project page
- OmniBOR third party integrations
- OmniBOR resources (GitBOM rename note)
- OmniBOR blog: A New OmniBOR Website (16 October 2024)
- omnibor-rs repository README
- omnibor-cli README
- omnibor-cli command line source (cli.rs)
- omnibor-cli on crates.io
- omnibor crate on crates.io
- OmniBOR GitHub organization repositories
- bomsh README
- IANA URI scheme registration: gitoid (provisional)
- Pro Git: Git Internals, Git Objects
- Git documentation: hash function transition
- CycloneDX 1.6 JSON reference
- SPDX 3.0.1: ExternalIdentifierType vocabulary
- SWHID.org
- SWHID specification v1.2: core identifiers
- Package URL specification
- CISA: Software Identification Ecosystem Option Analysis (October 2023)
- GCC manual: Option Index
- GNU ld manual: Command Line Options
Last reviewed: 24 October 2026. The OmniBOR specification is a draft and the CLI changes between releases; check the current specification and run omnibor --help on the version you install before building manifest generation into a release pipeline.