← Blog

Integrating CVE Auditing and SBOM Generation into CI/CD for Baremetal Embedded Systems (C++ / Rust)

A practical pipeline for generating CycloneDX SBOMs and auditing CVEs in CI/CD for mixed C++ / Rust baremetal firmware, in the context of the Cyber Resilience Act (CRA)

· 24 min read
embeddedrustc++ci-cdsecuritysbomfirmware

Integrating CVE Auditing and SBOM Generation into CI/CD for Baremetal Embedded Systems (C++ / Rust)

Why now: the CRA changes everything

The Cyber Resilience Act (EU Regulation 2024/2847), adopted in October 2024 and fully applicable in December 2027, requires every product containing digital components placed on the European market to provide:

  • a machine-readable SBOM for market surveillance authorities;
  • documented vulnerability management throughout the entire support period;
  • ENISA notification within 24 hours in case of an actively exploited vulnerability;
  • free security updates for the entire declared “support period”.

On the US side, EO 14028 and the NIST SSDF 800-218 push in the same direction. For essential entities, NIS2 adds another layer. No serious manufacturer will ship firmware without a structured inventory and CVE monitoring anymore.

Beyond regulatory obligation, there is a more pragmatic reality. The day a CVSS 9.8 CVE drops on mbedTLS on a Friday evening — and that day comes regularly — your ability to respond “we have twelve affected products, they are identified, patches ship Monday” instead of “we’ll check next week” makes the difference between a managed on-call and a panic weekend with the legal department. The SBOM is first and foremost for yourself, not for the regulator.

For baremetal embedded systems, this is particularly painful:

  • code is often vendored (vendor SDK copy-pasted into the repo);
  • no standard package manager for C++;
  • product lifecycles of 5 to 15 years on the market;
  • OTA updates not always possible (field-deployed devices, flash/RAM/energy constraints).

This article proposes a concrete pipeline to generate SBOMs and audit CVEs in CI/CD, with a focus on practices that scale in a mixed C++ / Rust baremetal project.

Understanding the artifacts

SBOM

An SBOM (Software Bill of Materials) is a structured inventory of a product’s software components. Two formats dominate:

  • SPDX (ISO/IEC 5962:2021): established, rich for licensing, serializable in JSON, YAML, RDF, tag-value.
  • CycloneDX (OWASP): security-oriented, JSON or XML, more compact, more widely adopted in CI/CD toolchains.

The CRA accepts both. CycloneDX is simpler to integrate in practice and its scanner ecosystem is more mature. It’s our default choice in this article.

CVEs and advisory databases

CVEs (Common Vulnerabilities and Exposures) are standardized identifiers, but their enrichment depends on several databases:

  • NVD (NIST): historical reference, but often lagging by several weeks since 2024.
  • OSV (Google): excellent Rust/Cargo coverage, GitHub Advisories, aggregates multiple sources, modern API.
  • GHSA: GitHub Security Advisories, primarily for GitHub-hosted ecosystems.
  • RUSTSEC: the Rust-specific database, integrated with cargo-audit.

In 2026, the sensible strategy is OSV as the primary source, complemented by NVD via a scanner like Trivy or Grype.

VEX

VEX (Vulnerability Exploitability eXchange) is a formal statement that a CVE does not affect your product (dead code, applied mitigation, specific configuration). Without VEX, a moderately large C++ project typically raises 200 to 400 alerts on the first full scan. The security team closes the tab thinking “we’ll look at it later”, the firmware team never reopens the report. This is the classic graveyard of well-intentioned SBOM programs. Investing in automatic VEX generation from the start radically changes the trajectory.

Two formats coexist:

  • CycloneDX VEX: VEX statements are embedded directly in the CycloneDX SBOM (or in a separate CycloneDX document of type vex). Advantage: a single format for both SBOM and VEX, no extra file to manage. Downside: more verbose, and the VEX is coupled to the SBOM lifecycle.
  • OpenVEX: an independent format (openvex.dev), backed by Chainguard and the Linux Foundation. A standalone JSON document, versioned separately from the SBOM. Advantage: you can update the VEX without regenerating the SBOM, and the specification is simpler to implement. It has the best support in OSV-Scanner (--experimental-vex flag).

In practice, OpenVEX is the most pragmatic choice today: a vex.json file versioned in the repo, natively consumed by OSV-Scanner and importable into Dependency-Track. This is the format used in the examples throughout this article.

Rust baremetal: the simplest ground

Cargo provides an explicit manifest, a lockfile, and an ecosystem entirely indexed on OSV. All SBOM/CVE tooling benefits immediately, including no_std cross-compiled targets.

SBOM generation

# CycloneDX format
cargo install cargo-cyclonedx
cargo cyclonedx --format json --target thumbv7em-none-eabihf

# SBOM embedded directly in the binary (key for embedded)
cargo install cargo-auditable
cargo auditable build --release --target thumbv7em-none-eabihf

cargo-auditable is a gem for baremetal: the SBOM is embedded in a dedicated ELF section (.dep-v0) of the produced binary. Three benefits:

  1. Post-deployment traceability: you can recover the inventory of a deployed firmware by analyzing the binary retrieved from the device.
  2. Robust audit chain: impossible to “lose” the SBOM along the way.
  3. Reliable OTA: each flashed image embeds its own truth.

Recovery from a deployed binary:

cargo install rust-audit-info
rust-audit-info firmware.elf

CVE audit

cargo install cargo-audit cargo-deny

# Quick scan against RUSTSEC
cargo audit

# Full policy check (licenses, sources, duplicates, CVEs)
cargo deny check

cargo-deny lets you encode your policy in deny.toml, versioned in the repo:

[advisories]
db-urls = ["https://github.com/rustsec/advisory-db"]
yanked = "deny"
unmaintained = "warn"
ignore = [
  # VEX justification required for each ignore
  { id = "RUSTSEC-2023-0071", reason = "RSA timing attack: no RSA in firmware, only Ed25519" }
]

[bans]
multiple-versions = "warn"
deny = [
  { name = "openssl", reason = "use rustls/ring in baremetal context" }
]

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC"]
confidence-threshold = 0.93

C++ baremetal: the real challenge

No universal package manager, and that’s where everything gets complicated. If you work on a baremetal project that’s a few years old, you probably recognize the scene: a third_party/ folder that looks like an attic, a copy of mbedTLS where nobody knows exactly which version (and which may have been locally patched in 2021, who knows), a vendor SDK delivered on a USB stick in 2019, a README that politely lies about versions. This is the norm, not the exception.

If you’re starting a greenfield project, package managers like Conan 2 or vcpkg can generate CycloneDX SBOMs from their dependency graphs. Conan 2 natively supports baremetal cross-compilation (toolchain profiles, os=baremetal) and has a built-in SBOM generator (SBOMGenerator). vcpkg is an option with custom triplets (arm-none-eabi.cmake), but the baremetal port ecosystem remains thin. These tools elegantly solve the SBOM problem when you can adopt them.

In practice, the majority of existing embedded projects use neither — and that’s the case covered below.

Vendored code: the reality of 80% of projects

Vendor SDK (STM32Cube, Nordic nRF SDK, NXP MCUXpresso, Renesas FSP) copied into third_party/, plus a few libraries (mbedTLS, MCUboot, FatFS, lwIP, tinyusb…). No lockfile, sometimes not even a version trace in the repo.

Pragmatic strategy in four steps:

1. Versioned manual manifest in third_party/MANIFEST.yaml:

components:
  - name: mbedtls
    version: 3.5.2
    license: Apache-2.0
    purl: pkg:github/Mbed-TLS/mbedtls@v3.5.2
    source_url: https://github.com/Mbed-TLS/mbedtls/releases/tag/v3.5.2
    sha256: 3e1be86b...
    modules_active: [aes, ecdh, ecdsa, sha256, ccm, gcm]
    modules_inactive: [rsa, dhe, md5, des, rc4, pkcs5, pkcs12, aria, camellia]

  - name: STM32CubeF4
    version: 1.28.0
    license: BSD-3-Clause-Clear
    purl: pkg:github/STMicroelectronics/STM32CubeF4@v1.28.0
    modules_active: [hal_gpio, hal_spi, hal_uart, hal_tim]
    modules_inactive: [hal_usb, hal_eth, hal_can, hal_i2s, hal_dac]

  - name: mcuboot
    version: 2.1.0
    license: Apache-2.0
    purl: pkg:github/mcu-tools/mcuboot@v2.1.0
    modules_active: [ecdsa_p256, swap_scratch]
    modules_inactive: [rsa_2048, direct_xip, ram_load]

The modules_active and modules_inactive fields are the cornerstone of automatic VEX. They are declared in the same place as the rest of the inventory — no need to parse each library’s headers. A single generic script reads these fields and produces not_affected statements for all components.

2. Automatic conversion to CycloneDX via script, triggered on every build:

# scripts/manifest_to_cdx.py
import yaml, json, uuid
from datetime import datetime, timezone

manifest = yaml.safe_load(open("third_party/MANIFEST.yaml"))

sbom = {
    "bomFormat": "CycloneDX",
    "specVersion": "1.5",
    "serialNumber": f"urn:uuid:{uuid.uuid4()}",
    "version": 1,
    "metadata": {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "component": {
            "type": "firmware",
            "name": "myproduct-firmware",
            "version": "1.2.3"
        }
    },
    "components": [
        {
            "type": "library",
            "name": c["name"],
            "version": c["version"],
            "purl": c["purl"],
            "licenses": [{"license": {"id": c["license"]}}],
            "hashes": [{"alg": "SHA-256", "content": c["sha256"]}]
                       if "sha256" in c else []
        } for c in manifest["components"]
    ]
}
print(json.dumps(sbom, indent=2))

3. Drift detection: a CI test checks that every subdirectory in third_party/ has a matching entry in the manifest, and vice versa. If someone adds a library without updating MANIFEST.yaml, the build breaks. This is the guarantee that the SBOM remains faithful to reality.

#!/usr/bin/env bash
# scripts/check_thirdparty_drift.sh
set -euo pipefail

MANIFEST="third_party/MANIFEST.yaml"

# Components declared in the manifest
DECLARED=$(yq -r '.components[].name' "$MANIFEST" | sort)

# Subdirectories tracked in git (no need for find, git already knows its tree)
PRESENT=$(git ls-tree --name-only HEAD third_party/ \
          | xargs -I{} basename {} | sort)

# Check for undeclared directories
UNDECLARED=$(comm -23 <(echo "$PRESENT") <(echo "$DECLARED"))
if [ -n "$UNDECLARED" ]; then
  echo "ERROR: directories in third_party/ missing from manifest:"
  echo "$UNDECLARED"
  exit 1
fi

# Check for ghost entries in the manifest
MISSING=$(comm -13 <(echo "$PRESENT") <(echo "$DECLARED"))
if [ -n "$MISSING" ]; then
  echo "ERROR: manifest entries with no matching directory:"
  echo "$MISSING"
  exit 1
fi

echo "OK: manifest and third_party/ are in sync."

We rely on git ls-tree rather than find: git already knows the tree structure, no need to scan the filesystem. Names in MANIFEST.yaml must match one-to-one with third_party/ entries in the git tree.

4. Automatic VEX generation from the modules_inactive fields in the manifest. A single script iterates over all components and produces OpenVEX not_affected statements for each declared inactive module — regardless of the library. No need to parse each library’s headers: the manifest is the single source of truth, and the script stays generic.

CVE audit in C++

Once the CycloneDX SBOM is produced, the tools converge and work identically on both the Rust and C++ SBOM:

# Trivy
trivy sbom sbom.cdx.json --severity HIGH,CRITICAL --exit-code 1

# Grype
grype sbom:sbom.cdx.json --fail-on high

# OSV-Scanner
osv-scanner --sbom=sbom.cdx.json

For baremetal, OSV-Scanner and Trivy as complements offer the best coverage: OSV for well-indexed OSS libraries, Trivy for broader NVD databases and less common components.

Complete CI/CD pipeline

GitHub Actions example, directly transposable to GitLab CI or Jenkins. It’s not perfect — it never will be — but it runs as-is on an existing project and improves incrementally. A rough pipeline that runs beats a perfect pipeline that doesn’t exist.

name: firmware-ci
on: [push, pull_request]

jobs:
  build-rust:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: thumbv7em-none-eabihf
      - run: cargo install cargo-auditable cargo-cyclonedx cargo-deny
      - name: Build with embedded SBOM
        run: cargo auditable build --release --target thumbv7em-none-eabihf
      - name: Generate CycloneDX
        run: cargo cyclonedx --format json -- --target thumbv7em-none-eabihf
      - name: Policy check
        run: cargo deny check
      - uses: actions/upload-artifact@v4
        with:
          name: sbom-rust
          path: bom.json

  build-cpp:
    runs-on: ubuntu-latest
    container: ghcr.io/myorg/arm-none-eabi-toolchain:13.2
    steps:
      - uses: actions/checkout@v4
      - name: Verify third_party integrity
        run: scripts/check_thirdparty_drift.sh
      - name: Build firmware
        run: |
          cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/cortex-m4.cmake
          cmake --build build
      - name: Generate SBOM from manifest
        run: python scripts/manifest_to_cdx.py > sbom.cdx.json
      - name: Generate VEX from build config
        run: python scripts/config_to_vex.py > vex.json
      - uses: actions/upload-artifact@v4
        with:
          name: sbom-cpp
          path: |
            sbom.cdx.json
            vex.json

  scan:
    needs: [build-rust, build-cpp]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - name: OSV scan with VEX
        uses: google/osv-scanner-action@v1
        with:
          scan-args: |
            --sbom=sbom-rust/bom.json
            --sbom=sbom-cpp/sbom.cdx.json
            --experimental-vex=sbom-cpp/vex.json
      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: sbom
          scan-ref: sbom-cpp/sbom.cdx.json
          severity: HIGH,CRITICAL
          exit-code: 1

  publish:
    needs: scan
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - name: Push SBOM to Dependency-Track
        run: |
          curl -X POST https://dtrack.myorg.com/api/v1/bom \
            -H "X-Api-Key: ${{ secrets.DTRACK_API_KEY }}" \
            -F "project=${{ env.PROJECT_UUID }}" \
            -F "bom=@sbom-cpp/sbom.cdx.json"
      - name: Sign artifacts with cosign
        run: |
          cosign sign-blob --yes sbom-cpp/sbom.cdx.json \
            --output-signature sbom.sig \
            --output-certificate sbom.crt

The severity gate

Three strategies, from most lenient to strictest:

  1. Warning only: log, don’t block. Only acceptable during the first weeks of rollout.
  2. Blocking at HIGH/CRITICAL: standard target practice.
  3. Blocking with mandatory VEX: every HIGH/CRITICAL blocks unless a justified VEX statement exists. This is the CRA target.

Expect three to six months between step 1 and step 3 on an existing project.

VEX in practice

A versioned vex.json file, OpenVEX format:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://myorg.com/vex/firmware-1.2.3",
  "author": "security@myorg.com",
  "timestamp": "2026-04-15T10:00:00Z",
  "statements": [
    {
      "vulnerability": {"name": "CVE-2024-XXXX"},
      "products": [{"@id": "pkg:github/myorg/firmware@1.2.3"}],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "Function mbedtls_pkcs5_pbes2 absent from build, MBEDTLS_PKCS5_C not defined"
    }
  ]
}

Four standard justifications to know: component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary.

Baremetal firmware specifics

Bootloader. MCUboot is quasi-standard. Track it in the SBOM with its version and its configuration (enabled crypto algorithms, TLV support, slot count). Most bootloader CVEs relate to configuration choices rather than the code itself.

Crypto. mbedTLS and wolfSSL concentrate the bulk of embedded CVEs. Check which modules are actually compiled, not the entire source repo. Declaring active and inactive modules in MANIFEST.yaml (modules_active / modules_inactive fields) and automatically generating not_affected VEX statements reduces noise by five to ten times in Dependency-Track.

Reproducibility. Pin the toolchain (versioned Docker image), compilation flags, and SOURCE_DATE_EPOCH for timestamps embedded in the SBOM. Without this, your SBOM drifts between two identical builds and your hashes no longer match the delivered artifacts. This is a CRA non-compliance pattern.

OTA traceability. Associate each signed OTA image with its SBOM in Dependency-Track. In case of a CVE published after delivery, you identify in minutes which deployed serial numbers need patching — instead of several days of manual investigation.

Constrained flash memory. On MCUs in the Cortex-M0/M0+ class, embedding the SBOM in the binary (cargo-auditable) costs between 1 and 5 kB. Budget for this from the design phase. On more constrained MCUs, store the SBOM off-device, indexed by firmware hash.

Open-source tool landscape

Here is the complete open-source ecosystem for building an end-to-end SBOM/CVE chain, without dependency on a commercial SaaS. Everything is self-hostable.

SBOM generation

  • Syft (Anchore) — Versatile generator: scans sources, archives, containers, and binaries. SPDX and CycloneDX output. Essential as a safety net to discover vendored components missing from the manifest.
  • cargo-cyclonedx — CycloneDX from a Cargo project, supports cross-compiled targets.
  • cargo-auditable — Embeds the SBOM in the ELF section of the Rust binary. No C++ equivalent.
  • cargo-sbom — Alternative to cargo-cyclonedx, SPDX output.
  • CycloneDX CLI (cyclonedx-cli) — Merge, validation, SPDX↔CycloneDX conversion, SBOM signing.
  • Conan 2 SBOM Generator — Native CycloneDX output from the Conan dependency graph.
  • ScanCode Toolkit (nexB) — License and provenance analysis by source code scanning. Heavy but thorough. Useful for characterizing a vendor SDK of unclear exact origin.
  • microsoft/sbom-tool — SPDX 2.2 generation, focusing on OS packages and common languages.
  • bom (kubernetes-sigs) — Simple SPDX tool, handy for scripting the composition of multiple SBOMs (firmware + bootloader + assets).

CVE scanning

  • Trivy (Aqua Security) — Versatile scanner: SBOM, containers, IaC, secrets. NVD + GHSA + Aqua databases. Excellent coverage, fast, native CI integration.
  • Grype (Anchore) — SBOM-oriented scanner (CycloneDX, SPDX, Syft JSON). Lighter than Trivy, perfect for vulnerability-focused pipelines. Well paired with Syft.
  • OSV-Scanner (Google) — Primary source OSV.dev, best latency on new Cargo CVEs. Native OpenVEX support.
  • cargo-audit — Rust-specific, RUSTSEC database.
  • cargo-deny — Extended Rust policy: CVEs, licenses, sources, duplicates, in a single deny.toml.

Quick comparison: Trivy / Grype / OSV-Scanner

CriterionTrivyGrypeOSV-Scanner
CVE sourcesNVD, GHSA, Aqua DBNVD, GHSA, Anchore DBOSV.dev (multi-source aggregator)
Accepted SBOM formatsCycloneDX, SPDXCycloneDX, SPDX, Syft JSONCycloneDX, SPDX, native lockfiles
New CVE latencyMediumMediumLow (OSV priority)
Rust / Cargo coverageGoodGoodExcellent
Vendored C/C++ coverageLimited (depends on purl)LimitedLimited
VEX supportOpenVEX (experimental)CycloneDX VEXNative OpenVEX
SpeedFastVery fastFast
FootprintModerate (embedded DB)LightLight
Ideal use caseVersatile pipeline (containers + SBOM + IaC)Pure SBOM-focused pipelineModern language coverage, VEX latency

Practical verdict: OSV-Scanner as primary for CVE latency and native VEX support, Trivy as secondary to extend NVD coverage on vendored C/C++ components. Grype remains excellent if you’re already in the Anchore chain (Syft + Grype + Quill for signing). Running all three in parallel is reasonable: combined runtime under two minutes, and cross-referencing eliminates blind spots.

Continuous management (platforms)

  • OWASP Dependency-Track — The open-source reference. Ingests CycloneDX SBOMs, rescans daily against NVD/OSV/GHSA, exposes REST API and webhooks. Simple Docker self-hosting. This is the recommended backbone for post-delivery monitoring.
  • DefectDojo — Broader DevSecOps platform: aggregates SAST (Static Application Security Testing — source code static analysis), DAST (Dynamic Application Security Testing — runtime security testing), SCA (Software Composition Analysis — third-party dependency inventory and audit), SBOM results, triage, metrics, Jira integration. Heavier to operate; relevant if you have other security streams to federate.

VEX and provenance

  • vexctl (Chainguard) — Generation, manipulation, attachment of OpenVEX documents.
  • openvex/go-vex — Go library for producing OpenVEX programmatically (useful for generating automatic VEX from your manifest).
  • in-toto — Attestation framework for the supply chain: who did what, when, on which artifact.
  • cosign (Sigstore) — Artifact signing (SBOM, firmware) without key management (keyless via OIDC) or with classic keys.
  • SLSA verifier — SLSA-level provenance verification.

Binary firmware analysis (baremetal-specific)

When a vendor delivers firmware without an SBOM, or to verify what is actually in the binary:

  • binwalk — Firmware extraction and analysis. Identifies sections, embedded filesystems, known signatures.
  • EMBA (Embedded Analyzer) — Automated firmware analysis platform: extraction, component identification, CVE scanning on detected components. Particularly suited for regulatory audits.
  • cwe_checker (Fraunhofer FKIE) — CWE pattern detection in binaries via Ghidra. Finds security bug classes through binary static analysis.
  • Ghidra (NSA) — Reference disassembler, extensible. For deep manual investigation.
  • radare2 / Cutter — Lighter alternatives to Ghidra.
  • FACT (Firmware Analysis and Comparison Tool) — Web-based firmware comparative analysis platform (useful for comparing two delivered versions).

Full example: auditing vendor firmware without an SBOM

Typical industrial scenario: a subcontractor delivers firmware.elf for integration on your board. No SBOM, just a vague changelog. Here is a reproducible investigation workflow in five steps, applicable to pure Cortex-M baremetal.

Step 1 — Component identification via strings

arm-none-eabi-strings firmware.elf > strings.txt

# Search for known library signatures
grep -iE "mbedtls|wolfssl|lwip|freertos|mcuboot|cmsis|tinyusb" strings.txt
# Typical output:
#   Mbed TLS 3.5.2
#   lwIP 2.1.3
#   MCUboot v2.1.0-rc1

Well-maintained C libraries almost always expose their version in a *_VERSION_STRING constant. This is the first identification source, reliable at 80%.

Step 2 — Structural analysis

# Sections, size, entry point
arm-none-eabi-objdump -h firmware.elf

# Symbols: confirms presence of specific modules
arm-none-eabi-nm firmware.elf | grep -E "mbedtls_|lwip_|sl_" | sort -u

# Embedded resource extraction (certificates, FAT, blobs)
binwalk -e firmware.bin

Symbol analysis provides a factual inventory of actually linked modules: if only mbedtls_aes_* and mbedtls_sha256_* symbols appear, that’s concrete evidence to demand from the vendor an SBOM and VEX covering only the modules actually present.

Step 3 — CWE pattern detection

docker run --rm -v $(pwd):/input fkiecad/cwe_checker:latest \
  /input/firmware.elf --json > cwe_report.json

# Filter critical findings
jq '[.[] | select(.severity == "high")] | length' cwe_report.json
jq '.[] | select(.severity == "high") | {name, address, description}' cwe_report.json

cwe_checker detects for example: unbounded strcpy usage, potential double-frees, unchecked pointer dereferences. Treat as indications, not as proof.

Step 4 — SBOM reconstruction

From identified components, feed a manifest dedicated to the vendor firmware:

# third_party/MANIFEST-vendor-firmware.yaml
firmware: vendor-acme-firmware
version: 4.2.1
sha256: 8a4f3c...

components:
  - name: mbedtls
    version: 3.5.2
    detection: strings + symbols
    confidence: high
    modules_active: [aes, sha256, ecdh, ecdsa]
    modules_inactive: [rsa, dhe, md5, rc4]   # auto VEX basis

  - name: lwip
    version: 2.1.3
    detection: strings
    confidence: high

  - name: mcuboot
    version: 2.1.0-rc1
    detection: strings (partial)
    confidence: medium                        # rc, to be confirmed

Step 5 — CVE scan and VEX generation

# Convert to CycloneDX
python scripts/manifest_to_cdx.py \
  third_party/MANIFEST-vendor-firmware.yaml \
  > vendor-sbom.cdx.json

# Automatic VEX generation from modules_inactive
python scripts/manifest_to_vex.py \
  third_party/MANIFEST-vendor-firmware.yaml \
  > vendor-vex.json

# Scan
osv-scanner --sbom=vendor-sbom.cdx.json \
            --experimental-vex=vendor-vex.json

Limitations to acknowledge. This workflow does not provide exhaustive coverage — impossible without the vendor’s source code. However, it does allow:

  • identifying components to track as a priority in CVE monitoring;
  • demanding an official SBOM from the vendor with concrete evidence as leverage;
  • building a defensive audit artifact in case of post-incident litigation;
  • blocking silent regressions between two deliveries (by automating the workflow in CI on every vendor drop).

About EMBA. EMBA orchestrates steps 1 through 3 in an automated fashion, but was designed for Linux or hybrid firmware (IoT gateways, routers). For pure Cortex-M baremetal without a filesystem, its value is marginal — the manual workflow above remains more relevant. EMBA comes into its own as soon as a filesystem (squashfs, jffs2, FAT) is present in the binary.

Complementary SAST (static analysis)

The SBOM covers third-party components (SCA), SAST covers your own code. Both are necessary for CRA compliance:

  • Semgrep (open core) — YAML rules, fast, community registry.
  • CodeQL — Free for open-source projects, paid for private.
  • cppcheck — Classic C/C++ static analysis.
  • clang-tidy — With cert-* and bugprone-* checkers, a solid foundation for embedded C++.
  • flawfinder, RATS — Detection of historically dangerous C function usage.
  • clippy — For Rust, shipped with rustup.

Licenses and compliance

  • FOSSology — Open-source license analysis platform at scale.
  • REUSE tool (FSFE) — Verifies that every source file in your repo has an SPDX-declared license and copyright header.
  • licensecheck — Lightweight license detection tool for a repo.

Minimum viable stack

For a team getting started, here is the recommended combination:

  1. SBOM: cargo-auditable + cargo-cyclonedx on the Rust side; manual manifest + CycloneDX script on the C++ side; Syft as a safety net on the final binary.
  2. Scanning: OSV-Scanner primary + Trivy or Grype complementary in CI.
  3. Platform: Self-hosted Dependency-Track for post-delivery monitoring.
  4. VEX: vexctl plus automatic generation from the build config.
  5. Provenance: cosign for signing SBOMs and firmware.
  6. Occasional binary audit: binwalk or EMBA on vendor firmware.

Plan three to four weeks of integration on an existing project.

Organizational best practices

Tooling is the easy part, and it’s nearly all this article has covered so far. The rest involves human decisions, and that’s where SBOM programs succeed or fail.

  • Version SBOMs like code, in a separate repo or a registry (Dependency-Track, or Cosign attestation on an OCI registry).
  • Automate post-delivery monitoring: Dependency-Track rescans published SBOMs daily against new CVEs. Without this, you miss CVEs that come out after your build, which is the most common case in embedded.
  • Separate CVE-as-build-gate from CVE-as-runtime-watch: the pipeline blocks regressions at the PR level, the continuous watcher triggers a Jira ticket for any in-service firmware affected by a new CVE.
  • Document every ignore and every VEX. Without written justification, the CRA audit is failed. The justification must point to technical evidence (config flag, code review, test).
  • Train your teams. SBOM debt is created in two days and takes six months to resolve. Include the topic in architecture reviews.
  • Measure. Three steering metrics: mean time between CVE publication and triage, ratio of justified VEX to raw ignores, SBOM coverage (percentage of vendored components present in the manifest).

Conclusion

The contrast between the two ecosystems is striking. On the Rust side, the SBOM/CVE pipeline is nearly turnkey: Cargo.lock provides the inventory, cargo-auditable embeds the SBOM in the binary, cargo-deny encodes the policy, OSV-Scanner scans against up-to-date databases — three days of work for a clean pipeline, and the ecosystem carries you. On the C++ baremetal side, every step requires custom tooling: manual manifest, CycloneDX conversion script, drift detection, VEX generation from the manifest. It works, but it’s fragile and depends on team discipline.

This gap should weigh in architectural decisions. For new projects, Rust in no_std offers a structural advantage: not only memory safety, but also the entire CVE audit and CRA compliance chain that comes “for free” with the Cargo ecosystem. For existing C++ projects, a gradual migration — starting with the most exposed components (crypto, networking, bootloader) — is a reasonable trajectory. This is the approach we follow at ADNT: our A/B bootloader for RP2040 is written in Rust, with an auditable Cargo.lock and an SBOM generated in a single command. SBOM and VEX in C++ remain necessary for legacy code, but every module migrated to Rust is one less module to manually maintain in the manifest.

Automating CVE scanning in CI/CD is not a luxury. It’s the only way to deliver on the CRA’s promise over the long term: firmware that lives ten years in the field cannot depend on an annual audit. The pipeline must run on every commit, the watcher must scan every night, and VEX must be generated without human intervention. Anything that isn’t automated will eventually be forgotten.

Start with the SBOM, even an imperfect one. An 80%-complete, well-automated SBOM beats a perfect SBOM generated manually once a year, every time. And if you have a choice of language for your next firmware project, the conclusion is clear: the Rust ecosystem is ready, the C++ ecosystem requires you to build it yourself.

The regulatory horizon is in eighteen months. That’s short to catch up, but more than enough to start properly.

Regulations

  • Cyber Resilience Act — Regulation (EU) 2024/2847, official text on EUR-Lex
  • NIS2 — Directive (EU) 2022/2555, EUR-Lex
  • Executive Order 14028Improving the Nation’s Cybersecurity, White House
  • NIST SSDF (SP 800-218)Secure Software Development Framework, NIST CSRC
  • NTIAThe Minimum Elements For a Software Bill of Materials, July 2021
  • ENISAGood Practices for Supply Chain Cybersecurity, 2023

Specifications

SBOM generation

CVE scanning

Continuous management platforms

VEX and provenance

Binary firmware analysis

SAST and licenses

Further reading