Casebook Chapter 02 Medical device SBOMs

Generating your first SBOM for a medical device.

How to create your first medical device SBOM. Start with cdxgen; switch to Syft for containers and EMBA for embedded Linux when you need to.

Alan ParkinsonAlan Parkinson
Last reviewed June 23, 20265 min read

The introductory guide covered what a medical device SBOM is, why it matters, and what regulators expect. This page is about how to create one. The first step is choosing a tool.

Choosing a tool

There are plenty of SBOM tools available, both commercial and open source, and you do need to evaluate which fits your device. My go-to open source tool is cdxgen, and most of this guide focuses on it. We'll look at others later, because not every medical device is the same. For Software in a Medical Device (SiMD) and embedded systems, EMBA and Yocto are good options. For container images, Syft. Your tooling choices need to match the device you're building.

Three types of SBOM tool

SBOM tools fall into three categories.

1. Scanners and generators

These tools create your initial SBOM. Examples include cdxgen, Yocto's create-spdx, the Maven CycloneDX plugins, EMBA, Syft, and Trivy.

They work in three different ways:

  • Build-time generation. The build system itself produces the SBOM as a side-effect of building the artefact. This is the most accurate option, because the build system has full visibility of every component, version, source, and licence.
  • Source and manifest scanning. A scanner reads dependency files (package.json, requirements.txt, go.mod, pom.xml, .csproj) and produces the SBOM from declared dependencies. Fast and accurate for what's declared. Can miss anything that isn't.
  • Binary and artefact analysis. A scanner reads compiled binaries, container images, or firmware images and identifies components by signature matching and package-manager metadata pulled out of the artefact. Catches what actually shipped, including things not in any manifest. Slower, noisier, and version detection can be approximate.

2. SBOM enrichment

Enrichment tools take an existing SBOM and add data from third-party sources: licence information, supplier metadata, support level and end-of-support dates, and component descriptions. This matters for medical devices, because the FDA expects a level of information in the SBOM that creation tools don't always populate.

3. Vulnerability monitoring

These tools match the components in your SBOM against vulnerability databases (NVD, OSV, GitHub Advisories, KEV) to surface known vulnerabilities and prioritise them using exploitability and severity data. Examples include Dependency-Track, Grype, and Threat Detective.

A note on tools that cover all three

Some tools cover more than one category, and commercial tools often cover all three. That isn't always an advantage. You can end up locked into a vendor's ecosystem, and the features you actually need for FDA compliance are sometimes locked behind enterprise pricing tiers.

A simple example

Take a small cloud product: a Python backend with a TypeScript and React UI. Two ecosystems, one deployment, all in the same repository.

That's the simplest kind of medical device codebase to put an SBOM around. cdxgen handles both ecosystems out of the box.

Installing cdxgen

cdxgen ships through several package managers. Pick whichever fits your workflow.

# npm npm install -g @cyclonedx/cdxgen # Homebrew (macOS / Linux) brew install cdxgen # winget (Windows) winget install cdxgen

Prebuilt binaries are also available on the cdxgen releases page. They're usually the cleanest choice for CI/CD pipelines, because they don't pull in Node.js or other runtime dependencies at build time.

Using cdxgen

Basic usage is to point cdxgen at a project directory. It will auto-detect the ecosystem in most cases:

cdxgen -o sbom.json ./your-project

To target a specific ecosystem explicitly, use the -t flag:

cdxgen -o sbom.json -t python ./your-project

The -t flag accepts ecosystem identifiers including python, javascript, java, dotnet, golang, android, cocoapods, and swift. The full list is in the cdxgen documentation. For polyglot projects, cdxgen will pick up multiple ecosystems from one invocation if pointed at the project root, so for the example above you can run it twice (once with -t python, once with -t javascript) or once against the repo root and let it auto-detect both.

A few ecosystem-specific things are worth flagging where they catch teams out:

  • Python (pip, Poetry, conda). Lockfile dependence matters. Without poetry.lock, Pipfile.lock, or a pip-tools-managed requirements.txt, the SBOM is approximate. conda packages have less mature support than pip; document any gaps.
  • JavaScript and TypeScript (npm, pnpm, yarn). Same lockfile dependence: without package-lock.json, pnpm-lock.yaml, or yarn.lock, the SBOM is approximate.
  • Java and Kotlin. cdxgen invokes the official Maven plugin under the hood for Maven projects, so output quality is high.
  • .NET / C#. Works against .csproj plus packages.lock.json. Watch for unpinned ("latest") versions and missing PURL or CPE identifiers in output; confirm both before treating the SBOM as submission-ready.
  • Native Android. cdxgen calls the CycloneDX Gradle plugin under the hood.
  • Native iOS. Reads CocoaPods (-t cocoapods) or Swift Package Manager (-t swift). Build-time native SBOM tooling for the Apple ecosystem is less mature than for Android; cross-check against the source manifest on the first build.
  • Cross-platform mobile (React Native, Flutter, Ionic) typically produces two SBOM-relevant layers: the JavaScript or Dart application code, and the platform-native dependencies bundled by the framework. Run cdxgen separately for each layer.

You have your first SBOM

You've created your first SBOM. It isn't yet ready for an FDA submission or for inclusion in a technical file for a notified body. The next step is enrichment, which is covered in the next guide. We'll look at the data the FDA expects to see in an SBOM and how to add it.

But there's unfinished business

Not every medical device is built with Python and TypeScript. Plenty of devices cover hardware, companion apps, and cloud deployments, sometimes all three at once. The rest of this guide walks through a more representative example.

One SBOM per deployment artefact

Every Software Item in the medical device's software architecture that contains or depends on third-party code needs to be inventoried. IEC 62304 organises software hierarchically: the Software System is decomposed into Software Items, and a Software Item can itself be composed of further Software Items, all the way down to Software Units.

You don't want one SBOM per leaf in that hierarchy. Software Items are typically grouped together at build time and ship together as a single deployment artefact: a firmware image, a built mobile application, a container image. The artefact is what you scan, sign, ship, and later monitor. The right unit of SBOM is the deployment artefact, with each SBOM covering every Software Item bundled into that artefact.

Take a representative infusion pump system. Its software architecture has four top-level Software Items:

  • The pump firmware running on the device hardware, connected to the companion app over Bluetooth Low Energy.
  • The companion app on iOS and Android.
  • The backend API written in Python.
  • The user interface built with JavaScript and npm.

Show Image

Three of those Software Items ship as standalone deployment artefacts (the firmware image, and the iOS and Android app builds). The backend API and the user interface ship together: they deploy as a single container on AWS Fargate, with the OS, the Python code, and the JavaScript code all packaged in the one image.

The companion app, the Python backend, and the JavaScript UI can each be scanned with cdxgen using the commands above. The Fargate container and the pump firmware need different tools, covered next.

Container images

The Fargate container packages the backend and UI as a single image. The right tool for it is Syft, run against the built image:

syft <registry>/<image>:<tag> -o spdx-json > sbom.json

A few things to watch for:

  • Scan the built image, not the Dockerfile. The image contains OS-level packages from the base layer that the Dockerfile alone does not declare.
  • For multi-stage builds, scan the final stage. Intermediate stages contain build tools that aren't in the deployed product.
  • Distroless and scratch-based images produce smaller SBOMs. That's correct, not a tool failure.
  • Default Syft to SPDX output. A 2025 Montana State University study found Syft's SPDX output scores substantially higher on NTIA compliance than its CycloneDX output across a sample of over 2,000 Docker Hub images.

cdxgen can also scan container images, by delegating to Trivy under the hood. It works well when Trivy is available. Syft avoids the extra dependency and is the simpler default for a first container SBOM.

Embedded firmware

The pump firmware sits at the more challenging end. The right tool depends on what the firmware actually is.

Embedded Linux

For embedded Linux Software Items, EMBA is the default. It extracts the firmware image, identifies installed packages and binaries, and produces a CycloneDX JSON SBOM alongside its other security analysis. It works against firmware from any build system (Yocto, Buildroot, OpenWrt-derived, custom pipelines, legacy or third-party firmware) and is especially useful when the build environment isn't accessible.

sudo ./emba -l ~/log -f ~/firmware.bin -p ./scan-profiles/default-sbom.emba

Installation is Docker-based:

git clone https://github.com/e-m-b-a/emba.git cd emba sudo ./installer.sh -d

The installer needs substantial bandwidth and disk space on first run. A Kali Linux or Debian-based host is recommended.

Things to watch for: version reliability drops for binaries without package-manager metadata; bootloader components are minimally covered; encrypted or proprietary firmware formats may resist extraction. EMBA is best-effort, not a completeness guarantee.

Yocto-built firmware

When the team has access to the Yocto build, Yocto's native create-spdx class is the more accurate option, because the build system has full knowledge of every component, its version, its source, and its licence (information EMBA has to infer from the binary).

Enable the class in conf/local.conf:

INHERIT += "create-spdx"

Distribution-level enablement uses INHERIT_DISTRO += "create-spdx".

The SPDX version produced depends on the Yocto release: Kirkstone, Nanbield, and Scarthgap (5.0) default to SPDX 2.2; Scarthgap can opt into SPDX 3.0 with INHERIT += "create-spdx-3.0"; Styhead (5.1) and later default to SPDX 3.0.

The output appears in the deploy directory. Scarthgap produces a compressed IMAGE-MACHINE.spdx.tar.zst archive of per-recipe SPDX documents; Styhead produces a single IMAGE-BASENAME-MACHINE.rootfs.spdx.json file.

create-spdx does not automatically capture binary dependencies that are dynamically linked but not declared in recipes, or filesystem files added outside the build. Running EMBA against the same firmware as a cross-check is worth it on the first build to surface anything create-spdx missed.

Buildroot-built firmware

Buildroot added native CycloneDX SBOM support in early 2025. The output is CycloneDX JSON, produced by a script rather than as a build target:

make show-info | utils/generate-cyclonedx > sbom.json

It's less mature than Yocto's tooling. No SPDX output natively (use the CycloneDX CLI's cdx2spdx if SPDX is required), transitive dependency capture is less precise than Yocto's, and the script has to be run manually after the build. For Buildroot devices today, EMBA is still a credible primary, with the Buildroot script as a build-time complement or cross-check.

Bare-metal C/C++ on RTOS

For FreeRTOS, Zephyr, and vendor RTOSes on small MCUs, no automated discovery exists. There's no centralised package manager and no extractable filesystem. The practical answer is a structured component list converted to a valid SBOM at build time.

Maintain the list (CSV, JSON, or YAML) in version control alongside the codebase. Update it on every component addition or version bump. Convert it with the CycloneDX CLI:

cyclonedx-cli convert --input-file components.csv --output-format json --output-file sbom.json

It's more manual than the other ecosystems by design. The discipline maps cleanly onto the IEC 62304 SOUP and COTS list you're already maintaining for the technical file.

The Fargate container: one SBOM or three?

Most artefacts get one SBOM each. The Fargate container is the case that needs a moment of thought, because it bundles two Software Items (the Python backend and the JavaScript UI) along with an OS layer. You have a real choice about granularity.

The minimum is one SBOM for the container. A Syft scan of the built image captures the OS packages, the bundled UI files, and the bundled backend code in a single output. Syft is strong on the OS layer; for the Python and JavaScript inside the image, it's doing artefact analysis on already-built files rather than reading the source projects directly, which is a less direct read of the language-level dependencies. This is the floor, and it's acceptable when set-up simplicity matters more than per-ecosystem accuracy.

The fuller version is three SBOMs. The container scan still covers the OS layer. A Python SBOM from cdxgen reading the backend project directly captures Python dependencies more accurately than the container scan can. A JavaScript SBOM from npm sbom or cdxgen does the same for the npm tree. The fidelity gain comes from matching the tool to the ecosystem, not from the count.

The right path depends on whether the container scan alone picks up enough. When it captures the language-level dependencies that matter for vulnerability monitoring and regulatory review, the container-only path is a valid choice and the set-up saving is real. When the scan leaves gaps (most commonly in transitive Python or JavaScript dependencies, which scanner-on-image analysis sometimes misses), the multi-tool path isn't optional; it's required for an honest SBOM. Either way, document in the technical file what was scanned, with which tool, and against which artefact.

Merging into a Software System SBOM

A Software System SBOM, when one is needed (some submission workflows ask for one, some customers expect one), is built by merging the per-artefact SBOMs.

The CycloneDX CLI handles this for CycloneDX inputs:

cyclonedx-cli merge \ --input-files firmware-sbom.json mobile-sbom.json fargate-sbom.json \ --output-file system-sbom.json

The SPDX tools project has equivalents for SPDX inputs.

If some Software Items produce SPDX (Yocto-built firmware) and others produce CycloneDX, pick a target format and convert before merging. Conversion is lossy in places, particularly for custom fields and annotations, so it's worth documenting which tool produced which portion of the merged output.

Provenance is worth preserving more broadly. Noting which Software Item each component originated from helps with audits and with debugging when a vulnerability lands in a component shared across Software Items.

What's next

You have SBOMs for every deployment artefact in the device. That's the goal of this guide. The next steps each get their own dedicated page:

  • Enrichment. Adding the data the FDA expects, including support level, end-of-support dates, and supplier metadata. This is the next guide in the series.
  • CI/CD automation. Regenerating SBOMs on every release without manual effort.
  • Vulnerability monitoring. Matching SBOM components against vulnerability databases as new CVEs (Common Vulnerabilities and Exposures) are disclosed, and prioritising findings.
  • Post-market storage. Keeping released SBOMs alongside the binaries and firmware images for the product's regulatory lifetime.

For now, you have the SBOMs. Next stop, enrichment.

Free scorecard · 18 questions

How good is your SBOM process?
Find out in four minutes.

Eighteen questions on how your SBOM is made, trusted, watched and defended. No documents needed, and you get a graded report with a prioritised plan by email.