Skip to main content

Introducing FlakeAudit, a CLI for evaluating CycloneDX SBOMs against custom policies

Introducing FlakeAudit, a CLI for evaluating CycloneDX SBOMs against custom policies

Earlier this year, we introduced Determinate Secure Packages (DSP), our first major effort to revolutionize the software supply chain. This offering takes an ever-expanding curated package set based on Nixpkgs and adds a wide variety of industry-leading features, including SLA-backed CVE remediation, optional Federal Information Processing Standards (FIPS) support, and full binary cache coverage in FlakeHub Cache.

A few weeks ago, we took things another step forward with FlakeBOM, a CLI for generating SBOMs from Nix flakes. FlakeBOM turns the “what is actually in this software?” question into a concrete CycloneDX v1.5 document that you can hand to a vulnerability scanner, an auditor, or your own internal tools.

Today, we’re announcing FlakeAudit, a CLI for evaluating CycloneDX SBOMs against custom policies that pairs perfectly with FlakeBOM. But first, a bit of explanation about why an additional tool is necessary (and powerful).

The limits of SBOMs

An SBOM on its own is only half the story. You can produce the most exhaustive document in the world and it still wouldn’t tell you whether the components it lists are actually acceptable within your organization: whether the licenses line up with your org’s policy, whether any open vulnerabilities have already been triaged by your security team, and so on. Without a scanning layer, an SBOM is a report on what exists on a system rather than something you can act on—and any true supply chain solution needs to empower you to act.

Open source SBOM auditing tools do exist. Dependency-Track and Trivy, for example, are both excellent in what they are aiming to accomplish, but both leave certain things to be desired in a Nix-first pipeline. Dependency-Track is a full OWASP project: a Java backend, a frontend, and an API server, all of which you have to stand up, harden, and keep on a supported version yourself before you can audit a single SBOM. For a team that just wants a CI gate, that’s a lot of operational weight to take on. Trivy is much lighter, but when it scans an SBOM it looks up vulnerabilities by Package URL (PURL), so it can only be as accurate as the PURL coverage in the SBOM you feed it. Some packages don’t carry a PURL at all.

On the proprietary side, most major vendors tend to bundle SBOM production, signing, and scanning into a single platform, and they assume your SBOMs live inside their system. But for an organization that already generates SBOMs locally, that’s a non-starter: you don’t get to bring your own SBOM, and you don’t get to apply your own policy without re-implementing it in their DSL.

Once you have an SBOM in hand, the natural next question is: “What do I do with it?” This is where the existing landscape of tools gets a bit dicey. Most vulnerability scanners are either proprietary, sluggish, written in bulky runtimes (like Dependency-Track in Java), or tied to a vendor platform that wants you to upload your build artifacts to their cloud. Each of those has its place, but none of them are a great fit for a Nix-powered team that’s already producing SBOMs locally and wants to gate release on the result of SBOM audits.

The other side of the problem is policy. Even after vulnerabilities are surfaced, you still need to express things like:

  • “Any component released under license X is not allowed in this product.”
  • “We need to ban this whole set of components, though with a per-component allowlist for exceptions.”
  • “We can only consume packages from these PURL types.”
  • “An exploitable vulnerability at high severity or above must block this release.”

You can write all of that down in a Markdown policy document, but it can only have teeth if a tool actually evaluates the SBOM against it and either passes or fails your CI run.

Enter FlakeAudit

Like FlakeBOM, FlakeAudit is a single Rust binary called flakeaudit, and it ships in the same statically linked form for Apple Silicon (aarch64-darwin), Linux on 64-bit Arm processors (aarch64-linux), and Linux on 64-bit x86 processors (x86_64-linux). It consumes any CycloneDX 1.5 JSON SBOM and currently exposes three subcommands:

  • check: evaluate the SBOM against a flakeaudit.toml policy and fail CI on violations.
  • compare: diff two SBOMs to show added/removed components or emit a coverage report against a provided baseline.
  • scan: query select databases for vulnerabilities affecting the SBOM’s components and write them back into the SBOM as CycloneDX vulnerabilities. To date, FlakeAudit supports querying the National Vulnerability Database (NVD) as well as osv.dev, with RUSTSEC, GHSA, npm advisories, and many others on our roadmap.

A typical invocation looks like this:

Running FlakeAudit against an SBOM and policy
flakeaudit check

If no flakeaudit.toml is present in the current directory, flakeaudit check writes a fully scaffolded starter file (with comments!) and exits, so you can get from “I have an SBOM” to “I have a policy I can iterate on” in a single command.

What a vulnerability actually looks like in a scanned SBOM

Before we get into how flakeaudit check evaluates a policy, it’s worth looking at what the vulnerabilities block of a CycloneDX SBOM actually contains. This is the part that enables a scanner to turn raw CVE data into something your policy can reason about.

When you run flakeaudit scan against an SBOM that has Common Platform Enumeration (CPE) identifiers on its components, FlakeAudit queries the NVD by CPE and writes the matching advisories back into the SBOM. When you point it at osv.dev instead, it uses the components’ PURLs. The output is a list of entries like this one:

JSON entry for an untriaged vulnerability
{
"id": "CVE-2025-71308",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-71308"
},
"references": [
{
"id": "https://git.kernel.org/stable/c/2611c9616cb52d3ed54a6095d72d18e645a6955a:reference",
"source": {
"url": "https://git.kernel.org/stable/c/2611c9616cb52d3ed54a6095d72d18e645a6955a"
}
},
{
"id": "https://git.kernel.org/stable/c/97f27573837ef96b4ba42af463cc800cab615c0e:reference",
"source": {
"url": "https://git.kernel.org/stable/c/97f27573837ef96b4ba42af463cc800cab615c0e"
}
}
],
"description": "In the Linux kernel, the following vulnerability has been resolved:\n\naccel/amdxdna: Fix potential NULL pointer dereference in context cleanup\n\naie_destroy_context() is invoked during error handling in\naie2_create_context(). However, aie_destroy_context() assumes that the\ncontext's mailbox channel pointer is non-NULL. If mailbox channel\ncreation fails, the pointer remains NULL and calling aie_destroy_context()\ncan lead to a NULL pointer dereference.\n\nIn aie2_create_context(), replace aie_destroy_context() with a function\nwhich request firmware to remove the context created previously.",
"created": "2026-05-27T14:16:43.403+0000",
"published": "2026-05-27T14:16:43.403+0000",
"updated": "2026-06-25T21:04:11.710+0000",
"affects": [
{
"ref": "/nix/store/j9c8wq77jqrvqfj8p6fwipraffkyp7rb-linux-6.18.37.drv"
},
{
"ref": "/nix/store/1abnffrgky3imkscsmxj0jfdrmnd05zv-linux-6.18.37.drv"
},
{
"ref": "/nix/store/bpzpjfizd8q62plxrarbcmr7i6cksb6v-linux-6.18.37.drv"
},
{
"ref": "/nix/store/ry8bn3z3gl4pdh30l0fnh7ykq54hqi1b-linux-6.18.37.drv"
}
]
}

At this point you have a vulnerability, you know which components in your SBOM it affects, but you don’t have any signal about whether your team has looked at it. For a scanner, that means treating it as untriaged by default, which is exactly what most policies want to fail on.

Compare that with what a vulnerability looks like once it has been triaged by us:

JSON entry for a triaged vulnerability
{
"id": "CVE-2025-71306",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-71306"
},
"advisories": [
{
"title": "Upstream fix (stable)",
"url": "https://git.kernel.org/stable/c/ab3d16da982a4ebb715d487dbf9dd66e3990d935"
},
{
"title": "Upstream fix (mainline)",
"url": "https://git.kernel.org/stable/c/377cae9851e8559e9d8b82a78c1ac0abeb18839c"
},
{
"title": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-71306"
},
{
"title": "SUSE",
"url": "https://www.suse.com/security/cve/CVE-2025-71306.html"
}
],
"analysis": {
"state": "not_affected",
"justification": "protected_by_compiler",
"detail": "Stack-out-of-bounds in ima_appraise_measurement via is_bprm_creds_for_exec(), only compiled when CONFIG_IMA=y. CONFIG_IMA is not set in the default NixOS kernel configuration for x86_64-linux or aarch64-linux. Offending commit 95b3cdafd7cb7 present in 6.18. Fix not backported to 6.18; patch does not apply cleanly due to process_measurement() signature change."
},
"affects": [
72 collapsed lines
{
"ref": "/nix/store/5fqcp313s0b7r5b44g2qnwvmmdm01bdc-linux-7.0.14.drv",
"versions": [
{
"range": "vers:nix/7.0.14",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/dywvznbhg06jzs0ikvk2p48q5wpl41rl-linux-7.0.14.drv",
"versions": [
{
"range": "vers:nix/7.0.14",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/hfshhcbs0k4lkc2qvi89497rxk7p0i60-linux-7.0.14.drv",
"versions": [
{
"range": "vers:nix/7.0.14",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/vivpq14lxsi2s6slssxz3fydk9yl54nq-linux-7.0.14.drv",
"versions": [
{
"range": "vers:nix/7.0.14",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/1abnffrgky3imkscsmxj0jfdrmnd05zv-linux-6.18.37.drv",
"versions": [
{
"range": "vers:nix/6.18.37",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/bpzpjfizd8q62plxrarbcmr7i6cksb6v-linux-6.18.37.drv",
"versions": [
{
"range": "vers:nix/6.18.37",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/j9c8wq77jqrvqfj8p6fwipraffkyp7rb-linux-6.18.37.drv",
"versions": [
{
"range": "vers:nix/6.18.37",
"status": "unaffected"
}
]
},
{
"ref": "/nix/store/ry8bn3z3gl4pdh30l0fnh7ykq54hqi1b-linux-6.18.37.drv",
"versions": [
{
"range": "vers:nix/6.18.37",
"status": "unaffected"
}
]
}
]
},

The analysis block is where the work of operating a secure package set actually lives. This is Vulnerability Exploitability eXchange (VEX) information, and it’s the data that enables a checker to know which vulnerabilities exist and, more importantly, which ones might pose a problem.

Policy-driven auditing

Your desired FlakeAudit policy is expressed in a single flakeaudit.toml file. The file is versioned for the sake of forward compatibility and it has four sections: licenses, bans, sources, and vulnerabilities. Every section takes a small set of rules, each of which is driven by a flexible ComponentMatcher that targets components by their bom-ref, name, version, purl, or cpe (with glob patterns on the string fields). Three actions—allow, warn, and deny—are wired to exit codes to make CI integration more straightforward.

Here’s an example of what a policy file might look like:

flakeaudit.toml
version = 1
[licenses]
unlicensed = "allow"
unmatched = "warn"
unknown = "warn"
allow = [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"OpenSSL",
"Python-2.0",
"Unicode-DFS-2016",
"Unicode-DFS-2020",
"Unlicense",
"Zlib",
]
[bans]
multiple-versions = "allow"
[sources]
missing-purl = "allow"
unmatched = "warn"
[[sources.allow]]
purl-type = "cargo"
[[sources.allow]]
purl-type = "pypi"
[[sources.allow]]
purl-type = "npm"
[[sources.allow]]
purl-type = "github"
[[sources.allow]]
purl-type = "gem"
[[sources.allow]]
purl-type = "hackage"
[[sources.allow]]
purl-type = "golang"
[[sources.allow]]
purl-type = "maven"
[vulnerabilities]
severity-floor = "high"
[vulnerabilities.state]
exploitable = "deny"
untriaged = "deny"
in-triage = "warn"
resolved = "allow"
resolved-with-pedigree = "allow"
not-affected = "allow"
false-positive = "allow"

A few things worth calling out:

  • License policy parses Software Package Data Exchange (SPDX) expressions. 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 we don’t recognize produce an unknown outcome, and components with a recognized license that isn’t in the allow list produce an unmatched outcome. Each one of those outcomes maps to one of the three actions independently, so you can warn on the noisy ones and only deny on the ones that actually matter to your compliance posture.
  • bans enables you to target components with matchers and override them with allow. [[bans.deny]] entries use the same ComponentMatcher shape as everything else, so you can write rules like bans.deny = [{ name = "log4j", version = "<2.17.1" }] and immediately follow it up with a per-component [[bans.allow]] rule for the exception you actually have. There’s also a multiple-versions rule for the case where the same component appears in more than one version in the SBOM, which is something that FlakeAudit catches automatically.
  • sources is a PURL allowlist. Components in a FlakeBOM-generated SBOM carry PURLs whenever they can be unambiguously mapped to a known ecosystem (for example the vendored dependencies inside a fetchNpmDeps derivation). The [[sources.allow]] rules enable you to restrict which ecosystems are acceptable for your build, with optional narrowing by PURL namespace.
  • vulnerabilities is VEX aware. severity-floor ignores advisories below the configured severity, and the state map applies the standard CycloneDX VEX states: exploitable, in_triage, untriaged, resolved, resolved_with_pedigree, not_affected, and false_positive. Each one maps to one of the three actions, so a policy like the one above can fail on untriaged (when something has been newly discovered by FlakeAudit), warn on in_triage (we’re tracking the fix, or exploitability depends on external factors), and silently allow resolved and friends.

When flakeaudit check runs against an SBOM and a policy like this, every component in the SBOM is flattened (including nested components, like 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.

Nix-aware SBOM diffing

The other half of FlakeAudit is compare, which is the subcommand that enables you to ask “what changed?” between two SBOMs. Two methods are available: diff, which subtracts components from the baseline, like a set difference, and coverage, which reports which components in the target are present in the baseline. 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 the ones that 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 how you check whether a refactor in one part of the dependency graph has rippled into a downstream component. SBOMs generated using different tools that don’t carry that metadata still work with the other four matchers, so FlakeAudit doesn’t lock you into a FlakeBOM-only world.

Adding your own vulnerability metadata

Determinate Secure Packages expresses its curated VEX metadata 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 Determinate Secure Packages 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. A typical entry, drawn from how we triage libsndfile, looks like this:

meta = {
# ...
vulns = {
CVE-2025-52194 = {
analysis = {
detail = "No updates since 2025 - https://github.com/libsndfile/libsndfile/issues/1082";
state = lib.vulns.state.inTriage;
};
};
CVE-2024-50613 = {
analysis = {
detail = "No updates since 2024 - https://github.com/libsndfile/libsndfile/issues/1034";
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. 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 given derivation under the affects attribute. That’s how a one-line call ends up as the structured analysis block FlakeAudit needs to take a vulnerability out of untriaged.

When a team needs to override our 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 that you’d compose any other derivation attribute and flakeaudit check picks up the result:

Overriding DSP's vulnerability metadata in a downstream overlay
final: prev: {
libsndfile = prev.libsndfile.overrideAttrs (old: {
# Having a patch containing the vulnerability identifier is already enough
# for FlakeBOM to exempt this derivation...
patches = [
./CVE-2025-52194.patch
];
meta = (old.meta or {}) // {
vulns = (old.meta.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;
};
};
};
};
});
}

This is the same pattern you may be used to when, for example, backporting package updates or overriding compile options. Once the overlay is in place, the next flakebom run picks up the new metadata (including your analysis) via evaluation, flakeaudit scan adds any missing information while also flagging still-affected components, and flakeaudit check against the policy above stops reporting that vulnerability as untriaged.

Note that for more traditional SBOM-based workflows, FlakeBOM also supports bringing your own VEX metadata directly.

Final thoughts

The story behind FlakeAudit is the same as the story behind FlakeBOM and Determinate Secure Packages: they produce maximal impact when used together. While FlakeAudit by itself certainly does offer value in helping you audit your SBOMs, it greatly benefits from the granular metadata provided by FlakeBOM, especially when run against flakes using Determinate Secure Packages instead of standard Nixpkgs.

We’ve been using FlakeAudit internally as part of our regular package audits for a while now and we’re pleased to share that FlakeAudit is now in early testing and available to Determinate Secure Packages customers. If you’d like to see how Determinate Secure Packages, FlakeBOM, and FlakeAudit could fit into your infrastructure, you can schedule a demo or ask questions at sales@determinate.systems or find us on Discord.

Written by

Dominique Hummel

Dominique is a (mainly) Rust and Nix engineer who also has a strong passion for additive manufacturing and EDM. He is also an active contributor to Nixpkgs and maintainer of packages such as the Linphone suite and the Blackmagic kernel drivers.