Generating an SBOM
Produce a CycloneDX or SPDX Software Bill of Materials for a project's dependencies with xgrep sbom — fully offline.
Generating an SBOM
xgrep sbom builds a Software Bill of Materials by discovering a project's
dependency manifests and lockfiles (21 ecosystems),
parsing them with xgrep's bundled package-file parsers, and emitting a standard SBOM
document:
xgrep sbom .The [path] is optional — omit it to scan the current directory, the same as
xgrep scan. This holds for every --include set, including the crypto (cbom)
and AI (aibom) inventories, so xgrep sbom --include aibom . and
xgrep sbom --include aibom do the same thing.
The scan is fully offline for a local path or an artifact on disk — it reads files only, never invoking a package manager. (A deployed-function target is the one exception: xgrep downloads that function's package first. See Artifacts and deployed functions below.) The output is CycloneDX JSON by default and is written to stdout, so you can pipe or redirect it:
xgrep sbom --output sbom.cdx.json .
For the JavaScript ecosystems (npm, pnpm, yarn) the SBOM carries the fidelity a
lockfile declares: integrity hashes (component.hashes / SPDX checksums), the
package→package dependency graph (CycloneDX dependencies / SPDX DEPENDS_ON),
and dependency scope (production vs development). Other ecosystems gain these
fields as their lockfile parsers add support; a package without them simply omits
the field.
Components are identified by Package URL (purl), which is also the key used to match dependencies against known vulnerabilities. CPE identifiers are no longer emitted — if you previously matched components on CPE, switch to purl.
Production dependencies by default.
xgrep sbomreports only production dependencies — development-only packages (npmdevDependenciesand their closure) are excluded unless you pass--include-dev. A dev-only package is not part of the deployed artifact, and the development tree is usually far larger than the runtime one.
Artifacts and deployed functions
The [path] does not have to be a source tree. xgrep sbom — and
xgrep deps list, deps tree, deps license and deps notice — accept the
same targets xgrep scan does:
# A Java build artifact: its own coordinates, plus every jar it bundles.
xgrep sbom app.jar
xgrep sbom target/myservice.war
# An archive.
xgrep sbom release.zip
# A PHP phar: the packages the build bundled into it.
xgrep sbom build/checkout-api.phar
# A deployed function: the dependencies actually deployed, layers included.
xgrep sbom lambda://checkout-api
xgrep sbom gcpfn://checkout-api
# A compiled Go binary — no source, no go.mod, no lockfile needed.
xgrep sbom ./checkout-api
# A Rust binary built with `cargo auditable`.
xgrep sbom ./payments-apiThis answers a question a source tree cannot. A fat jar carries its dependencies inside it, so its inventory is in the artifact rather than in a build file — and a deployed function's dependencies may live in a layer that is not in the function's own package at all.
Two things worth knowing:
- A jar states its coordinates in its metadata. Where a bundled library has had that metadata stripped (a shaded uber-jar), it cannot be identified from the artifact alone; the scan reports how many such archives it found rather than returning a shorter list that looks complete.
- A phar reports what it bundles, not itself. A phar states an alias and no
name, version or license, so there is nothing to report as the artifact's own
identity. The packages it ships are read from the
composer.lockand installed-package metadata the build wrote into it, which is the inventory a deployed phar actually has. - A function target uses the network, unlike every other SBOM target: it fetches the deployed package (and its layers) using your existing AWS credentials, through read-only API calls.
Compiled Go binaries
A deployed Go service is usually a single binary with no go.mod, no lockfile
and no source beside it. The Go linker writes the module graph into the
executable, and it survives stripping — so xgrep reads the dependency set
straight out of the binary:
xgrep sbom ./checkout-api
xgrep deps list ./checkout-apiThe answer is the same one go version -m gives, and it is complete enough for
vulnerability matching. Where a module is replaced, xgrep reports what was
actually linked, not what the go.mod asked for — a fork or a patched build
gets matched against its own version rather than the upstream one.
Binaries inside an archive, image or artifact are inventoried too, because everything in an extracted artifact is what ships. Binaries lying around a source checkout are not: there, a compiled file is the build you last ran, often at versions your source no longer uses, and reporting it would describe your working directory rather than your project. Point xgrep at the binary itself when you want it read.
A main module built outside a tagged release is reported without a version — the
toolchain writes (devel) there, which is a placeholder rather than something a
vulnerability can be matched against.
This is inventory only. xgrep does not analyse compiled machine code for vulnerabilities; what it reports for a binary is what the binary contains, not what it does.
Compiled Rust binaries
A Rust binary can carry its own crate graph, but only if it was built to. The
cargo auditable wrapper
writes the resolved graph into a linker section, and xgrep reads it:
cargo auditable build --release # once, in your build pipeline
xgrep sbom ./target/release/payments-api
xgrep deps list ./target/release/payments-apiThe result is the same crate set cargo audit bin and rust-audit-info report,
and it survives strip. ELF, Mach-O (including universal binaries) and PE are
all read, so a Linux container image, a macOS release and a Windows .exe are
inventoried the same way.
Two things this data says that a Go binary's does not:
- Direct and transitive are distinguished. The payload records which crates
the binary's own
Cargo.tomlasked for, so--scope directmeans something for a Rust binary. - Build-time crates are marked as such. A proc macro or a
build.rshelper ran on the machine that produced the artifact and is not in it, so it is reported as a development-scoped dependency and kept out of the default SBOM.--include-devbrings it back.
When the binary was not built with cargo auditable, there is nothing to
read, and xgrep says so rather than reporting an empty inventory:
$ xgrep deps list ./payments-api
error: "./payments-api" is a native binary that states no dependencies: it carries
neither Go build info nor cargo auditable data (`cargo auditable build`). Nothing
is inferred from a binary's strings, so there is nothing to inventoryThat refusal is deliberate. A Rust binary's strings hold crate names from panic
messages and fragments of Cargo.toml from build scripts, and a dependency list
assembled out of those would be a guess presented as an inventory — with no way
for you to tell which rows were read and which were invented. Rebuild with
cargo auditable and the answer becomes a real one.
The same rules as for Go binaries apply to where they are found: a binary inside an archive, image or artifact is inventoried, one lying in a source checkout is not, and naming the binary itself always works.
Output formats
Choose a format with --format:
| Format | Description |
|---|---|
cyclonedx-json | CycloneDX, JSON (default) |
cyclonedx-xml | CycloneDX, XML |
spdx-json | SPDX, JSON |
spdx-tag-value | SPDX, tag-value text |
json | xgrep's native JSON |
table | Human-readable list, grouped by package |
xgrep sbom --format spdx-json --output sbom.spdx.json .
xgrep sbom --format table . # quick look in the terminalThe CycloneDX output is enriched with component evidence and CPEs, so it drops into any tool that consumes CycloneDX.
Licenses in the SBOM
Each component carries the license xgrep resolved for it, from all three signals the license check uses: what the manifest declares, what the package metadata on disk states, and what the source shipped with the dependency says.
The middle one is what makes a Java, Python or .NET document useful at all. A
pom.xml describes its own artifact rather than its dependencies,
requirements.txt carries no metadata, and an SDK-style .csproj lists package
references and nothing else — so for those ecosystems "what the manifest
declares" is empty by construction. The statement lives in the package metadata
a build already wrote to disk: a .nuspec in the NuGet global-packages folder,
a POM in the local Maven repository, installed Python distribution metadata.
xgrep reads those files directly — no package manager is invoked and no request
is made, so the offline guarantee holds. These answers come from build state
rather than from your repository, though: the same tree on a machine that has
never built the project resolves differently. --no-local-repos switches all of
it off, for a document that must rest only on what the manifests themselves
state — the same flag, meaning the same thing, as in
deps license.
That matters because most lockfiles declare nothing. Reading only them leaves a document that comes back nearly empty on a tree xgrep has fully resolved:
$ xgrep sbom --format cyclonedx-json --no-license-detect . # manifests only
1 of 5 components carry a license
$ xgrep sbom --format cyclonedx-json --license-full .
4 of 5 components carry a licenseWhere the two signals disagree, the more restrictive license is recorded.
A dependency that declares MIT and ships an AGPL-3.0-only license file is
recorded as AGPL: putting the permissive value in a compliance document would
assert a grant the shipped code does not make. This is the same rule
deps license reconciles by, so the document and the compliance verdict can
never report different licenses for a package.
A component whose license could not be determined from either signal carries none, rather than an assumption.
| Flag | Effect |
|---|---|
| (default) | Manifests, plus SPDX-License-Identifier headers and license files shipped with dependencies. |
--license-full | Also identify a license by matching license-file text against the embedded SPDX corpus. |
--no-license-detect | Report only declared licenses; do not read the source shipped with dependencies. |
--no-local-repos | Do not read declared licenses from installed package metadata (Python dist-info, local package repositories and environments outside the tree). |
Detection works with --ref too. The tree at a commit carries whatever was
committed, so a repository that vendors its dependencies gets the same concluded
licenses for a past release as for the working tree. One that does not commit
them has nothing at that commit to read, and those packages keep whatever their
manifests declared.
Flags
| Flag | What it does |
|---|---|
--format <fmt> | Output format (see above). Default cyclonedx-json. |
--output <file> | Write to a file instead of stdout. |
--include <kind,…> | Bill-of-materials content to emit: sca (dependencies, the default), cbom (cryptographic assets), aibom (AI libraries/models). See Combined bills of materials. |
--ref <commit/tag/branch> | Read files from a git ref instead of the working tree. |
--direct-only | Emit only direct dependencies, dropping transitive ones. |
--license-full | Also identify licenses by matching license-file text against the embedded SPDX corpus. |
--no-license-detect | Report only what manifests declare, skipping the shipped source. |
--include-dev | Include development-scoped dependencies (e.g. npm devDependencies and their dev-only closure). By default only production dependencies are reported. |
--ecosystem <eco,…> | Restrict to named ecosystems, e.g. go,npm. Default: all. |
--exclude-dir <dir,…> | Directory names to prune from the scan, matched on any path segment (e.g. testdata,fixtures). Additive to the built-in vendored-dir skips. |
# A direct-dependency SBOM for a release tag, in SPDX
xgrep sbom --ref v1.4.0 --direct-only --format spdx-json --output release-sbom.json .
# Only the JavaScript and Go dependency sets
xgrep sbom --ecosystem npm,go .Combined bills of materials
xgrep sbom emits a Software BOM (your dependencies) by default. --include lets
you add — or swap in — other kinds of bill of materials:
| Kind | Content |
|---|---|
sca | Software dependencies from manifests and lockfiles (the default). |
cbom | A Cryptography BOM: the cryptographic assets (algorithms, keys, protocols, certificates) detected in source. |
aibom | An AI BOM: the AI SDKs, frameworks, runtimes, and models detected in source. |
The set you list is the selector. Combine kinds to get a single CycloneDX 1.6 document; list one kind to get just that inventory:
# Dependencies + cryptographic assets + AI inventory, one merged CycloneDX document
xgrep sbom --include sca,cbom,aibom --output bom.cdx.json .
# Just the cryptographic inventory (no software dependencies)
xgrep sbom --include cbom .cbom and aibom are detected by scanning your source (heavier than the offline
dependency parse), so any set containing them emits CycloneDX JSON only — the SPDX
and table formats apply to a plain sca bill of materials. --include-dev,
--direct-only, and --ecosystem refine the sca content and require it to be
selected. --ref and --exclude-dir span the whole set: --ref builds every
selected inventory for that commit/tag/branch (the crypto/AI scan reads the ref's
tree too), and --exclude-dir prunes the same directories from both the
dependency parse and the crypto/AI scan.
When a package shows up in more than one inventory — an AI SDK that is also one of your dependencies, say — the merged document lists it once: the dependency entry (with its version and hashes) is kept and annotated with the AI classification, and any model that uses it points at that single component.
In CI
Generate an SBOM as a build artifact and attach it to releases or feed it to a downstream SBOM consumer:
xgrep sbom --format cyclonedx-json --output sbom.cdx.json .In a GoReleaser release
GoReleaser can generate an SBOM for each release from an
sboms block. Point it at xgrep
and it becomes your SBOM generator — GoReleaser attaches the document to the
release and folds its checksum into checksums.txt:
# .goreleaser.yaml
sboms:
- id: cyclonedx
artifacts: any # one SBOM for the project, not one per archive
cmd: xgrep
args: [sbom, '..', --format, cyclonedx-json, --output, '${document}']
documents:
- '{{ .ProjectName }}_{{ .Version }}.cyclonedx.sbom.json'Two things to know:
- GoReleaser runs the command from the
dist/directory, so scan..(the repo root), not.. ${document}is GoReleaser's own substitution for the output filename (the entry indocuments, where Go templates like{{ .ProjectName }}are allowed). Withartifacts: anythe command runs once for the whole project, so${artifact}— the per-archive variable from GoReleaser's default example — isn't available; name the document from templates instead.
If your repository embeds dependency manifests you don't want in the SBOM (for
example test fixtures), drop them from the walk with --exclude-dir:
args:
[
sbom,
'..',
--exclude-dir,
'testdata,fixtures',
--format,
cyclonedx-json,
--output,
'${document}',
]To answer "which of these dependencies does our code actually use?", pair the SBOM with dependency queries.
Overview
xgrep is a Software Composition Analysis (SCA) tool — rank dependency CVEs by whether your code can actually reach them, generate a CycloneDX or SPDX SBOM, trace dependency usage offline, and upgrade what matters, across 21 ecosystems.
Generating an AIBOM
Produce a CycloneDX 1.6 AI Bill of Materials with xgrep sbom --include aibom — an inventory of the AI/LLM SDKs, frameworks, inference runtimes, and models a codebase uses, across 16 languages, fully offline.