← Back
Last updated on

From Regulation to Pipeline: Auditing Cyber Resilience Act Compliance with cra-audit

Miguel Gonzalez Herrera Miguel Gonzalez Herrera

On 11 September 2026, the first obligations for manufacturers under the Cyber Resilience Act (Regulation (EU) 2024/2847) started to apply, but only the reporting obligations of Art. 14 (Art. 71(2)). From that date, manufacturers of products with digital elements must notify any actively exploited vulnerability contained in their product that they become aware of, simultaneously to the CSIRT designated as coordinator and to ENISA. The clock starts when the manufacturer becomes aware of the exploitation: an early warning within 24 hours, a notification within 72 hours and a final report no later than 14 days after a corrective or mitigating measure is available. Severe incidents affecting the security of the product follow a similar procedure (Art. 14(3)). The duty also covers products placed on the market before December 2027 (Art. 69(3)). The rest of the regulation, including the essential cybersecurity requirements of Annex I and the manufacturer obligations of Art. 13, applies from 11 December 2027.

First, a note on scope. The CRA covers products with digital elements placed on the EU market: commercial npm libraries and SDKs, desktop and mobile apps, firmware, downloadable software. A pure SaaS or a website generally falls outside it (NIS2 is more likely the relevant framework there), unless it is the remote data processing solution of a product, such as the backend a connected device or an app needs to work.

If your software is in scope, this affects you more than you might expect. A typical JavaScript application is mostly other people’s code. Under the CRA, an “actively exploited vulnerability” is one with reliable evidence that a malicious actor has exploited it in a system (Art. 3(42)), not necessarily in yours. So a known exploited flaw in a component you ship can be a vulnerability contained in your product, if it is actually exploitable there (Art. 3(40)). From December 2027 the regulation adds an explicit due diligence duty for third-party components, open-source ones included (Art. 13(5)), and requires you to report vulnerabilities you find in a component to whoever maintains it (Art. 13(6)).

The problem is that the regulation speaks the language of law, not the language of package-lock.json. Phrases like “without known exploitable vulnerabilities” or “identify and document vulnerabilities and components, including by drawing up a software bill of materials” do not tell a developer which command to run, which format to use or which fields to fill. That gap is exactly what I wanted to close.

What is cra-audit

cra-audit is an open-source compliance audit tool for the CRA and the German Federal Office for Information Security’s technical guideline BSI TR-03183. It is available as an npm package and as a GitHub Action on the Marketplace.

npx cra-audit

That’s it: no installation, no configuration required. It reads your lockfile natively (npm, Yarn classic and Berry, pnpm), audits the exact versions your package manager pinned and exits with code 1 if something the regulation cares about is wrong. For projects in Python, Java, Go, Rust, .NET, PHP or Ruby, you pass the SBOM generated by your usual tooling (Syft, cdxgen, the CycloneDX plugins) with -i, and you get the same checks.

Mapping the law to checks

The core idea of the tool is simple: every check is tied to a specific provision, or to the BSI guidance on how to meet it. This is the mapping:

RequirementWhat cra-audit checks
Annex I Part II (1) — document components through an SBOM covering at least the top-level dependenciesGenerates an SBOM in CycloneDX 1.6 (or SPDX 2.3) with every direct and transitive dependency, and validates it against the data fields of TR-03183-2 v2.1.0 (best practice): creators, timestamps, purls, SHA-512 hashes, licences, dependency graph and completeness
Annex I Part I (2)(a) — no known exploitable vulnerabilitiesQueries OSV.dev for every direct and transitive dependency, with severity, CVE aliases and the fixed version
Art. 14 — report actively exploited vulnerabilitiesCross-references findings with the CISA Known Exploited Vulnerabilities catalogue, flags them for assessment and shows the 24 h / 72 h / 14 days deadlines that apply if it is exploitable in your product
Art. 13(5) — due diligence on third-party componentsFlags compromised releases from the OpenSSF malicious packages dataset (e.g. the Shai-Hulud worm versions); these always fail and cannot be allowlisted
TR-03183-2 (best practice) — licence governance and integrityVerifies that every dependency has a documented licence that complies with your allow/deny policy
Annex I Part II (5)–(6), Art. 13(8), Annex IIChecks for a SECURITY.md, a vulnerability reporting contact and a defined support period, plus a valid security.txt (RFC 9116, best practice)

To be clear about what is law and what is guidance: the CRA itself only asks for a machine-readable SBOM covering at least the top-level dependencies. BSI TR-03183 is a German technical guideline that goes further, and it is not mandatory. security.txt is not mandatory either. cra-audit applies both because they are the most concrete public answer to “what does a good SBOM and a good disclosure channel look like”, not because the regulation names them.

When a dependency with an actively exploited vulnerability shows up, the output does not just print a CVE. It asks you to assess exploitability first, and shows the deadlines the law sets if the vulnerability turns out to be exploitable in your product:

  HIGH     [KEV] vite@6.2.3  (fix: vite@6.4.3)
    GHSA-4r4m-qw57-chr8 / CVE-2025-31125 Vite has a `server.fs.deny` bypassed … [KEV since 2026-01-22]

⚠ CRA Art. 14 — applies if this known exploited (CISA KEV) vulnerability is exploitable in your product
    Assess first: is the vulnerable code shipped and exploitable in your product?
      • Not exploitable ...... record it as not_affected with VEX (cra-audit vex)
      • Exploitable .......... notify the coordinating CSIRT and ENISA through the Single Reporting Platform (SRP):
          Early warning ...... within 24 hours of becoming aware
          Notification ....... within 72 hours
          Final report ....... within 14 days after a corrective measure is available

A KEV match does not start the 24-hour clock by itself. It tells you a vulnerability in your tree has been exploited somewhere. From there, you have to assess whether the vulnerable code is present and exploitable in your product. If it is, you are aware of an actively exploited vulnerability contained in your product, and Art. 14 applies. If it is not, document why with a VEX statement (see below).

This example shows why the assessment matters. CVE-2025-31125 only affects projects that expose the Vite dev server to the network with --host or server.host. If what you ship is the production build, the vulnerable code is not in your product at all: the right outcome is a not_affected VEX statement, not a notification to ENISA. If you do expose a dev server, for example in a preview environment, upgrade and assess. (The suggested vite@6.4.3 covers every advisory open against vite@6.2.3: cra-audit picks the highest of their fixed versions. This CVE alone is fixed in 6.2.4.)

Not every vulnerability is exploitable: VEX

Anyone who has run npm audit on a real project knows the feeling: dozens of findings, most of them in code paths your product never touches. The CRA does not ask you to have zero CVEs in your tree; it asks you not to ship exploitable ones. The difference has to be justified and documented.

That is what VEX (Vulnerability Exploitability eXchange) is for, and TR-03183-2 reserves it as the place for vulnerability data, outside the SBOM. In cra-audit you record your assessment in the policy file:

{
  "vulnerabilities": {
    "allowlist": [
      {
        "id": "CVE-2020-11023",
        "package": "jquery",
        "status": "not_affected",
        "justification": "code_not_reachable",
        "detail": "We never pass untrusted HTML to jQuery DOM methods; content is rendered via textContent."
      }
    ]
  }
}

The audit accepts not_affected and false_positive assessments, keeps failing on affected and under_investigation, and exports everything to a CycloneDX 1.6 or OpenVEX 0.2.0 document:

npx cra-audit vex -o vex.cdx.json

From a legal point of view, this is the part I find most valuable. An allowlist entry is no longer a silent // ignore in a config file; it is a reasoned, machine-readable statement that you can hand to a market surveillance authority.

Running it on every commit

Compliance that depends on someone remembering to run a command is not compliance. The GitHub Action turns the audit into a gate in your pipeline and publishes the evidence:

name: CRA audit
on: [push, pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  cra:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - uses: migohe14/cra-audit@v2
        with:
          fail-on: high
          sbom: sbom.cdx.json
          vex: vex.cdx.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cra-evidence
          path: |
            sbom.cdx.json
            vex.cdx.json
            cra-audit.sarif

Every finding appears in Security → Code scanning through a SARIF report, pointing at the exact line of the lockfile. Malicious packages and KEV findings are errors; vulnerabilities you assessed as not_affected appear as suppressed, with your justification attached. And every run leaves behind a cra-evidence artifact with the SBOM, the VEX and the SARIF, which can feed the technical documentation the CRA expects you to keep.

One caveat: GitHub deletes workflow artifacts after 90 days by default, while the CRA requires technical documentation to be kept for at least 10 years after the product is placed on the market, or for the support period if that is longer (Art. 13(13)). Treat the artifact as a working copy: attach the SBOM and VEX to each GitHub Release, or archive them in storage you control.

For non-JavaScript projects, generate the SBOM first and pass it with sbom-input:

      - uses: anchore/sbom-action@v0
        with: { format: cyclonedx-json, output-file: sbom.cdx.json, upload-artifact: false }
      - uses: migohe14/cra-audit@v2
        with:
          sbom-input: sbom.cdx.json
          vex: vex.cdx.json

Starting from zero: readiness

Some CRA duties live in the repository itself, not in the dependency tree. Do you have a published way to receive vulnerability reports? Have you declared how long you will support the product? The readiness command checks this, and --init generates templates for SECURITY.md and .well-known/security.txt, prefilled from your package.json and including a coordinated disclosure process and the Art. 14 reporting commitments:

npx cra-audit readiness --init

It never overwrites existing files, and it keeps failing until you replace every TODO placeholder. A template is a starting point, not a policy.

Seeing the whole picture

Finally, npx cra-audit --visualize produces an interactive HTML report with every component: installed and latest version, licence, vulnerabilities and maintenance signals such as last publish, number of maintainers or, with --github, last commit, contributors and stars. Each package gets a maintenance score from 0 to 100. An abandoned package with a single maintainer is not a CVE yet, but it is exactly the kind of risk that due diligence is meant to catch.

Design decisions

Building a security tool for the software supply chain comes with an obvious irony: the tool itself becomes part of your supply chain. A few decisions follow from that:

  • Zero production dependencies. npx cra-audit pulls a single package. There is no dependency tree to compromise.
  • Minimal data disclosure. Only package names and versions are sent to api.osv.dev; the KEV catalogue is downloaded from cisa.gov. Your code never leaves your machine. The GitHub token, when used, only goes to api.github.com.
  • Fail loudly. If the KEV catalogue cannot be reached, the report says so instead of silently passing. If OSV.dev is down, it falls back to npm audit.
  • Self-contained reports. The HTML report embeds all its data and uses its own vanilla JavaScript, with no external CDNs. It works offline and is safe to share.

What it does not do

I want to be precise here, because it matters. cra-audit is a technical tool that verifies controls associated with the CRA and TR-03183. It does not constitute legal advice, and passing the audit does not mean your product is CRA-compliant. The regulation also requires a cybersecurity risk assessment, secure-by-design development, technical documentation, conformity assessment procedures, the CE marking and much more. Supply-chain transparency and vulnerability handling are one important piece of that puzzle, and they are the piece that code can check automatically.

It is also a check on known vulnerabilities only. An SBOM and an OSV lookup will not reveal a zero-day, and they will not tell you whether someone is exploiting your product right now. That takes runtime monitoring, incident response and a working channel for vulnerability reports. cra-audit covers what you can know at release time: that you are not shipping components with known exploitable vulnerabilities, and that you have the evidence to prove it.

There are also some known technical limits: Yarn Berry lockfiles do not store the npm tarball SHA-512, so those projects fail the hash check, and since TR-03183-2 v2.1 requires SPDX 3.0.1 or later, CycloneDX is the format to use for a conforming SBOM.

Try it

npx cra-audit

The project is MIT-licensed and lives at github.com/migohe14/cra-audit. If you run it on your project and something does not map well to your reality, or you read a provision differently than I do, open an issue. Turning law into code is an interpretive exercise, and it gets better with more readers.