Skip to Content

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 a flakeaudit.toml policy 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 CycloneDX vulnerabilities.

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:

Bootstrap a policy from an SBOM
flakeaudit check

If 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.allow and licenses.deny accept SPDX identifiers and full SPDX expressions, so something like Apache-2.0 WITH LLVM-exception is a first-class value rather than an opaque string.
  • Components with no license metadata produce an unlicensed outcome; components with a license FlakeAudit doesn’t recognize produce an unknown outcome; and components with a recognized license that isn’t in the allow list produce an unmatched outcome. Each of these outcomes maps independently to one of the three actions, so you can warn on the noisy ones and deny only on the ones that matter.

Bans

The bans section enables you to ban or allow specific components by targeting them with a ComponentMatcher.

  • bans.deny entries ban matching components from the SBOM.
  • bans.allow entries suppress a ban for specific components, letting you express a per-component allow-list for exceptions to a broader ban.
  • multiple-versions controls 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.allow rules restrict which ecosystems are acceptable for your build, optionally narrowing by PURL namespace.
  • missing-purl controls the outcome for components that have no PURL at all.
  • unmatched controls the outcome for components whose PURL doesn’t match any allow rule.

Vulnerabilities

The vulnerabilities section is VEX-aware.

  • severity-floor ignores advisories below the configured severity, so you can focus the audit on the vulnerabilities that matter. Values are ordered none < low < medium < high < critical.
  • state maps 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:

OptionSectionWhat it does
licenses.denylicensesA global list of denied SPDX identifiers/expressions, evaluated independently of allow. Use it to hard-fail on specific licenses anywhere in the SBOM.
licenses.exceptionslicensesComponent-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]]bansThe matcher-based ban and allow rules. The scaffold only shows the empty lists; you add the [[bans.deny]] and [[bans.allow]] tables.
[[sources.allow]] entriessourcesThe 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.ignorevulnerabilitiesA 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.denyvulnerabilitiesA list of advisories to explicitly fail on, regardless of their VEX state or the severity-floor.
ComponentMatcher fieldsall sectionsThe 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:

FieldDescription
bom-refGlob pattern against the component’s BOM reference.
nameGlob pattern against the component’s name.
versionA version constraint (semver range like <2.17.1, or =1.2.3 for exact matches).
purlGlob pattern against the component’s full PURL.
purl-typeThe PURL type to match (such as cargo, npm).
purl-namespaceThe PURL namespace to match (such as crates.io).
purl-nameThe PURL name to match (such as serde).
cpeGlob pattern against the component’s CPE string.
reasonOptional 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:

Restricting allowed package sources
[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:

Ban a component family while allowing an exception
[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:

License exception scoped to a component
[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:

Allowlisting and hard-denying specific advisories
[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.

Diffing two SBOMs by Nix deriver
flakeaudit compare sbom-baseline.cdx.json sbom-target.cdx.json \ --matcher deriver \ --method diff \ --output diff.json

The 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:

Scan an SBOM against NVD and osv.dev
flakeaudit scan --source nvd --source osv

When 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:

Overriding DSP's vuln metadata in a downstream overlay
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.

Last updated on