FlakeAudit
Access to FlakeAudit is granted as part of our Determinate Secure Packages (DSP) offering. Contact us at sales@determinate.systems to discuss terms of access or schedule a demo.
FlakeAudit is a Rust CLI that evaluates CycloneDX SBOMs against policies that you define in a flakeaudit.toml file.
An SBOM lists the components in your software; FlakeAudit checks those components, and the advisories that affect them, against your policy and reports whether they pass or fail.
A flakeaudit check that passes exits with a zero status, while one that fails exits non-zero, so that you can gate a build or release on the result.
FlakeAudit accepts any CycloneDX v1.5 JSON SBOM.
It provides the most robust information, however, when used on SBOMs generated by FlakeBOM against a flake that uses Determinate Secure Packages: FlakeBOM emits the Nix-specific metadata that powers FlakeAudit’s Nix-aware diffing, and DSP’s curated VEX metadata takes a vulnerability out of the default untriaged state.
Usage
FlakeAudit ships as a single statically linked binary called flakeaudit for aarch64-darwin, aarch64-linux, and x86_64-linux.
It currently exposes three subcommands:
check: evaluate an SBOM against aflakeaudit.tomlpolicy and fail CI on violations.compare: diff two SBOMs, or emit a coverage report against a baseline.scan: query vulnerability databases and write the results back into the SBOM as CycloneDXvulnerabilities.
By default, each subcommand reads sbom.cdx.json from the current directory and writes to flakeaudit.toml (for check) or compared.json/sbom.cdx.json (for compare/scan).
You can point any of them at different paths explicitly, and run flakeaudit <subcommand> --help for the full set of options.
Getting started
Run check in the directory containing your SBOM to generate a starter policy:
flakeaudit checkIf no flakeaudit.toml is present in the current directory, FlakeAudit writes a fully scaffolded starter policy (with explanatory comments) and exits, so you have a valid policy you can fill in immediately.
Every section of that scaffold is safe to omit if you don’t care about it since all fields have sensible defaults.
The policy file
Your desired FlakeAudit policy lives in a single flakeaudit.toml file.
The file is versioned for the sake of forward compatibility and is composed of four sections: licenses, bans, sources, and vulnerabilities.
Every section takes a small set of rules, each of which resolves to one of three actions that are wired directly to exit codes for CI:
deny: fail the audit check (FlakeAudit exits non-zero).warn: emit a warning but pass the check.allow: silently pass the check.
When flakeaudit check runs, every component in the SBOM is flattened (including nested components, such as the vendored dependencies inside an npm or Cargo derivation), every vulnerability in the SBOM’s vulnerabilities block is evaluated against the vulnerabilities section, and the results are printed to stdout.
If any rule resolves to deny, FlakeAudit exits non-zero and your CI fails.
Licenses
The licenses section governs component licenses.
licenses.allowandlicenses.denyaccept SPDX identifiers and full SPDX expressions, so something likeApache-2.0 WITH LLVM-exceptionis a first-class value rather than an opaque string.- Components with no license metadata produce an
unlicensedoutcome; components with a license FlakeAudit doesn’t recognize produce anunknownoutcome; and components with a recognized license that isn’t in theallowlist produce anunmatchedoutcome. Each of these outcomes maps independently to one of the three actions, so you canwarnon the noisy ones anddenyonly on the ones that matter.
Bans
The bans section enables you to ban or allow specific components by targeting them with a ComponentMatcher.
bans.denyentries ban matching components from the SBOM.bans.allowentries suppress a ban for specific components, letting you express a per-component allow-list for exceptions to a broader ban.multiple-versionscontrols what happens when the same component appears at more than one version in the SBOM, which FlakeAudit catches automatically.
Sources
The sources section is a Package URL (PURL) allow-list.
sources.allowrules restrict which ecosystems are acceptable for your build, optionally narrowing by PURL namespace.missing-purlcontrols the outcome for components that have no PURL at all.unmatchedcontrols the outcome for components whose PURL doesn’t match anyallowrule.
Vulnerabilities
The vulnerabilities section is VEX-aware.
severity-floorignores advisories below the configured severity, so you can focus the audit on the vulnerabilities that matter. Values are orderednone<low<medium<high<critical.statemaps each CycloneDX VEX state (exploitable,in-triage,untriaged,resolved,resolved-with-pedigree,not-affected,false-positive) to one of the three actions.
Config options not in the initial scaffold
The scaffold that flakeaudit check generates is meant as a starting point, so it fills in the scalar defaults and leaves the list-shaped rules empty.
The following options are not demonstrated by the initial scaffold, either because they hold no default entries or because they apply to the entries inside an empty list.
These are the options you’ll typically add as you move from the starter policy to one that matches your requirements:
| Option | Section | What it does |
|---|---|---|
licenses.deny | licenses | A global list of denied SPDX identifiers/expressions, evaluated independently of allow. Use it to hard-fail on specific licenses anywhere in the SBOM. |
licenses.exceptions | licenses | Component-specific license allowances. Each [[licenses.exceptions]] entry pairs a ComponentMatcher with an allow list, extending the global licenses.allow for just the matched components. |
[[bans.deny]] / [[bans.allow]] | bans | The matcher-based ban and allow rules. The scaffold only shows the empty lists; you add the [[bans.deny]] and [[bans.allow]] tables. |
[[sources.allow]] entries | sources | The scaffold emits an empty allow = []. Real policies add entries as [[sources.allow]] tables (see below) with purl-type, and optionally purl-namespace, qualifiers, and reason. |
vulnerabilities.overrides.ignore | vulnerabilities | A list of advisories to allowlist. Every [[vulnerabilities.overrides.ignore]] entry takes an advisory id (such as CVE-2024-1234) and an optional reason; listed vulnerabilities pass even if their state or severity would normally fail. |
vulnerabilities.overrides.deny | vulnerabilities | A list of advisories to explicitly fail on, regardless of their VEX state or the severity-floor. |
ComponentMatcher fields | all sections | The matcher targeting fields (bom-ref, name, version, purl, purl-type, etc.) that live inside bans.*, licenses.exceptions, and other rules. The scaffold never shows these because they’re part of the entries rather than top-level settings. |
Component matcher
Many rules (bans.deny, bans.allow, licenses.exceptions) target components with a ComponentMatcher.
All provided fields must match (AND logic), and glob patterns (*, ?) are supported for the string fields:
| Field | Description |
|---|---|
bom-ref | Glob pattern against the component’s BOM reference. |
name | Glob pattern against the component’s name. |
version | A version constraint (semver range like <2.17.1, or =1.2.3 for exact matches). |
purl | Glob pattern against the component’s full PURL. |
purl-type | The PURL type to match (such as cargo, npm). |
purl-namespace | The PURL namespace to match (such as crates.io). |
purl-name | The PURL name to match (such as serde). |
cpe | Glob pattern against the component’s CPE string. |
reason | Optional human-readable reason for the rule, surfaced in check output. |
Allowing an ecosystem
The most common way to fill out the sources section is to allowlist the PURL ecosystems your build consumes:
[sources]
missing-purl = "allow"
unmatched = "warn"
[[sources.allow]]
purl-type = "cargo"
[[sources.allow]]
purl-type = "pypi"
[[sources.allow]]
purl-type = "npm"
reason = "Frontend toolchain"Denying a component with an exception
Combining bans.deny and bans.allow enables you to express logic like “ban this set of components, except these specific ones.”
Here’s an example:
[bans]
multiple-versions = "warn"
[[bans.deny]]
name = "log4j"
version = "<2.17.1"
reason = "Known RCE class before 2.17.1"
[[bans.allow]]
name = "log4j"
version = "=2.17.1"
purl-namespace = "org.apache.logging.log4j"License exceptions
When a single component legitimately needs a license outside the global allow list, use licenses.exceptions instead of broadening the whole policy:
[licenses]
unmatched = "warn"
unlicensed = "warn"
unknown = "warn"
allow = ["MIT", "Apache-2.0"]
[[licenses.exceptions]]
name = "some-proprietary-tool"
allow = ["LicenseRef-Proprietary"]Overriding a specific advisory
Use vulnerabilities.overrides to carve out individual advisories from your policy:
[vulnerabilities]
severity-floor = "medium"
[vulnerabilities.state]
untriaged = "deny"
[[vulnerabilities.overrides.ignore]]
id = "CVE-2024-9999"
reason = "Component is compiled out in our configuration"
[[vulnerabilities.overrides.deny]]
id = "CVE-2025-0001"
reason = "Actively exploited in the wild"Comparing SBOMs
The compare subcommand enables you to ask “what changed?” between two SBOMs.
Two methods are available:
diff: Subtract the components that are in the baseline from the target, like a set difference.coverage: Report which components of the target are present in the baseline, along with a count of covered components and the missing entries.
Both methods take a matcher, and there are six matchers to choose from: bom-ref, deriver, cpe, purl, callstack, and name-version.
flakeaudit compare sbom-baseline.cdx.json sbom-target.cdx.json \
--matcher deriver \
--method diff \
--output diff.jsonThe deriver and callstack matchers are unique to FlakeAudit, because they’re only meaningful for SBOMs that carry the Nix-specific metadata that FlakeBOM emits (nix:narinfo:deriver on components, and evidence.callstack.frames for evaluation paths).
For a typical Nix-powered team, those matchers are how you compare a freshly-rebuilt SBOM against a known-good one, or check whether a refactor at one part of the dependency graph changed a downstream component.
SBOMs generated by other tools that don’t carry that metadata still work with the other four matchers, so FlakeAudit can be used with SBOMs from any source.
Scanning for vulnerabilities
The scan subcommand queries vulnerability databases for advisories affecting the SBOM’s components and writes them back into the SBOM as CycloneDX vulnerabilities entries.
Pass the --source argument (repeatable) to select which databases to query:
nvd: The National Vulnerability Database, keyed off each component’s CPE identifiers.osv: osv.dev, keyed off each component’s PURL.
flakeaudit scan --source nvd --source osvWhen you run scan against an SBOM that already carries VEX analysis (for example from Determinate Secure Packages), FlakeAudit flags any still-affected components and adds missing advisory information, so the scanned output reflects both what your security team has triaged and what the databases report.
Adding your own vulnerability metadata
The way Determinate Secure Packages expresses its curated VEX metadata is through the meta.vulns attribute of a Nix derivation.
Each entry in meta.vulns is keyed by an identifier (a CVE, GHSA ID, or similar) and contains an analysis block, as well as other optional attributes supported by the CycloneDX v1.5 spec, like advisories or recommendation.
The lib.vulns helpers that ship with DSP keep that analysis block well-formed by providing ergonomics that get translated into the CycloneDX format at eval time, which FlakeBOM includes in its SBOM for FlakeAudit to ingest:
meta = {
# ...
vulns = {
CVE-2025-52194 = {
analysis = {
detail = "No updates since 2025 - https://github.com/libsndfile/libsndfile/issues/1082";
state = lib.vulns.state.inTriage;
};
};
};
};The lib.vulns.state.*, lib.vulns.justification.*, and lib.vulns.response.* helpers each emit a small CycloneDX-shaped value, so you only have to write the human-readable bits.
For example, lib.vulns.justification.codeNotPresent "..." produces both a cyclonedx = "code_not_present" field and a comment field, and lib.vulns.state.resolved produces a cyclonedx = "resolved" field together with the right status for the given derivation under the affects attribute.
When a team needs to override DSP’s curation for its own builds, the same shape is what you reach for in an overlay.
Because meta.vulns is just an attribute set, you can compose it with the upstream set the same way you’d compose any other derivation attribute, and flakeaudit check picks up the result:
final: prev: {
libsndfile = prev.libsndfile.overrideAttrs (old: {
# Having a patch containing the vuln identifier is already enough
# for FlakeBOM to exempt this derivation...
patches = [
./CVE-2025-52194.patch
];
meta = (old.meta or {}) // {
vulns = (old.vulns or {}) // {
# ...but if you want more control over the final contents of your SBOM,
# you can also opt for the manual approach.
CVE-2025-52194 = {
analysis = {
justification =
lib.vulns.justification.codeNotPresent
"https://github.com/libsndfile/libsndfile/pull/1099 is in our build.";
state = lib.vulns.state.resolved;
};
};
};
};
});
}Once the overlay is in place, the next flakebom run picks the new metadata (including your analysis) up via evaluation, flakeaudit scan adds any missing information while also flagging still-affected components, and flakeaudit check against your policy stops reporting that vulnerability as untriaged.
For more traditional SBOM-based workflows, FlakeBOM also supports bringing your own VEX metadata directly via its --merge-vex option.