Threat Model and SBOM a Medical Device App
Build an FDA-compliant cybersecurity assessment package for a medical device.
Introduction
30 Second Summary
When a wound-imaging camera uses AI to predict healing outcomes, the software inside it becomes a patient safety concern. The FDA now requires proof that manufacturers have identified and managed cybersecurity risks before any connected device can reach the market.
In this project, you will perform a full cybersecurity assessment of a simulated medical wound-imaging device. You will produce three artifacts that FDA requires for device clearance: a threat model, a Software Bill of Materials, and a vulnerability scan report.
What You'll Build
A cybersecurity assessment package for a Python Flask application that simulates a medical wound-imaging device's backend API, assessed using the same open-source tools medical device companies use in production.
By the end of this project, you'll have:
- A STRIDE threat model diagram identifying threats and mitigations for the device's architecture, created in OWASP Threat Dragon.
- An FDA-compliant CycloneDX SBOM documenting every software component in the application, generated using Syft.
- A vulnerability scan report showing known CVEs and their severity ratings, produced by Grype.
- Secret Mission: Write a Coordinated Vulnerability Disclosure plan and remediation timeline aligned with FDA's Section 524B postmarket requirements.
Are there any prerequisites?
You need basic Linux terminal familiarity (navigating directories, running commands) and a basic understanding of web applications (HTTP, APIs). No prior cybersecurity or medical device experience is required.
Install the Cybersecurity Toolkit
Medical device cybersecurity engineers need a local toolchain that maps to the FDA's Secure Product Development Framework (SPDF). Three tools form the minimum viable toolkit: a threat modeler, an SBOM generator, and a vulnerability scanner.
In this step, you will install Syft for SBOM generation, Grype for vulnerability scanning, and OWASP Threat Dragon for threat modeling. You will also create a Python project environment with medical device dependencies.
In this step, get ready to:
- Install Syft and Grype for SBOM generation and vulnerability scanning.
- Download OWASP Threat Dragon for threat modeling.
- Set up a Python project environment with medical device dependencies.
Install Syft and Grype
Syft generates Software Bills of Materials (SBOMs) from your project's dependencies. Grype takes that SBOM and cross-references every component against vulnerability databases. Both tools are maintained by Anchore and ship as single compiled binaries with no external dependencies.
- Install Syft by running its official installer script:
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
- Install Grype by running its official installer script:
curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin
What do these commands do?
Each command downloads a shell script from Anchore's servers and pipes it to sh. The -b /usr/local/bin flag tells the installer where to place the binary.
The sudo prefix is needed because /usr/local/bin is a system directory that requires elevated permissions to write to.
- Verify both installations by running:
syft version
grype version
You should see version information for each tool, including fields like Application, Version, and Platform.
Seeing 'command not found'?
- Make sure you included sudo in the install commands. Without it, the binary cannot be placed in /usr/local/bin.
- Check that your terminal's PATH includes /usr/local/bin by running echo $PATH. If it is missing, restart your terminal session.
- Confirm your VirtualBox has internet access. The install scripts download binaries from Anchore's servers.
Still stuck? Help me troubleshoot why syft or grype isn't found after installing.
Install OWASP Threat Dragon
OWASP Threat Dragon is a free, open-source threat modeling tool that implements the STRIDE methodology. The AppImage format lets you run it on any Linux distribution without a package manager install.
- Download the Threat Dragon v2.6.0 AppImage by running:
wget https://github.com/OWASP/threat-dragon/releases/download/v2.6.0/Threat-Dragon-ng-2.6.0.AppImage
- Make the downloaded file executable by running:
chmod +x Threat-Dragon-ng-2.6.0.AppImage
What is an AppImage?
An AppImage bundles an entire application and its dependencies into a single file. You do not need to install it through apt or any package manager. Just make it executable and run it directly.
- Verify that Threat Dragon launches by running:
./Threat-Dragon-ng-2.6.0.AppImage
You should see the OWASP Threat Dragon application window appear on your screen. Close it after confirming it launched successfully. You will use it in a later step.
Seeing a FUSE error?
AppImages require FUSE (Filesystem in Userspace) to run. If you see a message about FUSE being missing, install the required library:
sudo apt install libfuse2
Then try running the AppImage again. This installs FUSE 2 alongside FUSE 3 without breaking other system packages.
Need more help? Help me get the Threat Dragon AppImage running on Ubuntu.
Set up the Python project environment
You need a realistic application to assess. In this substep, you will create a project directory with a Python virtual environment and install the dependencies that simulate a medical device backend.
- Install the Python virtual environment and pip packages by running:
sudo apt update && sudo apt install -y python3-venv python3-pip
Why python3-venv and python3-pip?
Ubuntu includes Python 3 by default, but the virtual environment module and pip are shipped as separate system packages. Virtual environments isolate your project's dependencies from the system Python, which is a basic security control in medical device development.
- Create your project directory and move into it by running:
mkdir ~/meddevice-security && cd ~/meddevice-security
- Create and activate a Python virtual environment by running:
python3 -m venv venv
source venv/bin/activate
Your terminal prompt should now show (venv) at the beginning, indicating the virtual environment is active.
- Install the medical device simulation dependencies by running:
pip install flask requests cryptography
Why these three packages?
- Flask is the web framework that exposes the device's API. It represents network attack surface.
- requests handles outbound HTTP calls to external systems (like an EHR). It broadens the dependency tree for your SBOM.
- cryptography handles data-at-rest encryption, an FDA security control. It pulls in native C libraries, which are common vulnerability targets.
- Lock your dependency versions into a requirements.txt file by running:
pip freeze > requirements.txt
Why pip freeze instead of writing requirements.txt by hand?
In medical device development, you never ship unpinned dependencies. The pip freeze command captures the exact versions of every installed package, including transitive dependencies.
The SBOM you generate later must reflect exactly what is installed, not what you hope is installed. This file becomes the ground truth for your software supply chain.
- Verify your requirements.txt was created by running:
cat requirements.txt
You should see a list of packages with pinned version numbers, including Flask, requests, cryptography, and their transitive dependencies like Werkzeug and Jinja2.
Seeing 'externally-managed-environment' error?
Make sure your virtual environment is active. Your terminal prompt should start with (venv). If it does not, re-run:
cd ~/meddevice-security
source venv/bin/activate
Then try the pip install command again. The error occurs when pip tries to install packages into the system Python, which newer Ubuntu versions block by default.
Need help? Help me fix my Python virtual environment setup on Ubuntu.
Your cybersecurity toolkit is ready. Next up, you will build the medical device simulation that gives you a realistic target to assess.
Build the Medical Device Simulation
Your cybersecurity toolkit is installed and ready. Now you need something to assess.
In this step, you will create a minimal Flask application that simulates a wound-imaging device's backend API. The FDA does not accept threat models created in the abstract. They must reference actual system components, which means you need a realistic target with real dependencies to scan.
In this step, get ready to:
- Create a Flask application simulating a medical device backend.
- Create a configuration file following secure development practices.
- Verify the application runs and responds to HTTP requests.
Create the application file
Spectral AI's DeepView system processes wound images through AI algorithms and returns healing predictions. Your simulation captures the essential architecture: receiving patient data over HTTP, processing it, and returning clinical assessments.
- In your text editor, create a new file called app.py inside your ~/meddevice-security directory.
- Copy the following code into app.py to set up the application skeleton and health endpoint:
"""
Medical Device Simulation - Wound Assessment API
Simulates a backend service for an AI-powered wound imaging device.
This is a security assessment target, not production medical software.
"""
import hashlib
import json
import logging
import os
from datetime import datetime, timezone
from flask import Flask, jsonify, request
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("wound_assessment")
def generate_assessment_id(patient_id, timestamp):
"""Generate a unique assessment identifier."""
raw = f"{patient_id}-{timestamp}".encode()
return hashlib.sha256(raw).hexdigest()[:16]
@app.route("/health", methods=["GET"])
def health_check():
"""Health check endpoint for monitoring."""
return jsonify(
{
"status": "operational",
"service": "wound-assessment-api",
"version": app.config["API_VERSION"],
"timestamp": datetime.now(timezone.utc).isoformat(),
}
)
What does this code do?
This sets up the application foundation and a simple health endpoint.
- from config import Config loads your application settings from a separate file (you will create this next).
- generate_assessment_id() creates a unique identifier for each wound assessment using a SHA-256 hash. This simulates how real medical devices track assessments.
- The /health endpoint returns the service status. Every production API needs a health check so monitoring systems can verify the service is running.
Now add the main business logic. Copy the following code below the health_check function in app.py:
@app.route("/assess", methods=["POST"])
def create_assessment():
"""
Submit wound image metadata for AI assessment.
In a real device, this would process the image through an ML model.
"""
data = request.get_json()
if not data:
logger.warning("Received empty request body")
return jsonify({"error": "Request body is required"}), 400
required_fields = ["patient_id", "wound_location", "image_hash"]
missing = [f for f in required_fields if f not in data]
if missing:
logger.warning("Missing required fields: %s", missing)
return jsonify({"error": f"Missing required fields: {missing}"}), 400
timestamp = datetime.now(timezone.utc).isoformat()
assessment_id = generate_assessment_id(data["patient_id"], timestamp)
assessment = {
"assessment_id": assessment_id,
"patient_id": data["patient_id"],
"wound_location": data["wound_location"],
"image_hash": data["image_hash"],
"timestamp": timestamp,
"healing_prediction": {
"probability": 0.78,
"confidence_interval": [0.65, 0.91],
"recommended_action": "monitor",
"reassessment_days": 7,
},
"model_version": app.config["MODEL_VERSION"],
}
logger.info(
"Assessment created: %s for patient: %s",
assessment_id,
data["patient_id"],
)
return jsonify(assessment), 201
@app.route("/assessments/<assessment_id>", methods=["GET"])
def get_assessment(assessment_id):
"""Retrieve a specific assessment by ID."""
logger.info("Assessment requested: %s", assessment_id)
return jsonify(
{"error": "Assessment not found", "assessment_id": assessment_id}
), 404
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
What does this code do?
These are the API endpoints that handle wound assessment data.
- The /assess endpoint accepts POST requests with wound image metadata (patient ID, wound location, image hash). It validates the input, generates a unique assessment ID, and returns a simulated healing prediction.
- The /assessments/<assessment_id> endpoint simulates a lookup route. It returns a 404 since this simulation does not persist data.
- The if __name__ == "__main__" block starts the server on 127.0.0.1:5000 with debug mode disabled. Debug mode is off because a real medical device would never expose a debugger.
- Save app.py.
Seeing a syntax error when you save?
- Check that the triple-quoted docstrings use three double-quotes """ not single-quotes.
- Verify that the f-string in generate_assessment_id uses curly braces around the variables: f"{patient_id}-{timestamp}".
- Make sure you have both code sections pasted in order, with the second block directly below the first.
help me fix a syntax error in my Flask app.py file
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py file for comparison:
"""
Medical Device Simulation - Wound Assessment API
Simulates a backend service for an AI-powered wound imaging device.
This is a security assessment target, not production medical software.
"""
import hashlib
import json
import logging
import os
from datetime import datetime, timezone
from flask import Flask, jsonify, request
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("wound_assessment")
def generate_assessment_id(patient_id, timestamp):
"""Generate a unique assessment identifier."""
raw = f"{patient_id}-{timestamp}".encode()
return hashlib.sha256(raw).hexdigest()[:16]
@app.route("/health", methods=["GET"])
def health_check():
"""Health check endpoint for monitoring."""
return jsonify(
{
"status": "operational",
"service": "wound-assessment-api",
"version": app.config["API_VERSION"],
"timestamp": datetime.now(timezone.utc).isoformat(),
}
)
@app.route("/assess", methods=["POST"])
def create_assessment():
"""
Submit wound image metadata for AI assessment.
In a real device, this would process the image through an ML model.
"""
data = request.get_json()
if not data:
logger.warning("Received empty request body")
return jsonify({"error": "Request body is required"}), 400
required_fields = ["patient_id", "wound_location", "image_hash"]
missing = [f for f in required_fields if f not in data]
if missing:
logger.warning("Missing required fields: %s", missing)
return jsonify({"error": f"Missing required fields: {missing}"}), 400
timestamp = datetime.now(timezone.utc).isoformat()
assessment_id = generate_assessment_id(data["patient_id"], timestamp)
assessment = {
"assessment_id": assessment_id,
"patient_id": data["patient_id"],
"wound_location": data["wound_location"],
"image_hash": data["image_hash"],
"timestamp": timestamp,
"healing_prediction": {
"probability": 0.78,
"confidence_interval": [0.65, 0.91],
"recommended_action": "monitor",
"reassessment_days": 7,
},
"model_version": app.config["MODEL_VERSION"],
}
logger.info(
"Assessment created: %s for patient: %s",
assessment_id,
data["patient_id"],
)
return jsonify(assessment), 201
@app.route("/assessments/<assessment_id>", methods=["GET"])
def get_assessment(assessment_id):
"""Retrieve a specific assessment by ID."""
logger.info("Assessment requested: %s", assessment_id)
return jsonify(
{"error": "Assessment not found", "assessment_id": assessment_id}
), 404
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
Create a configuration file
In a real medical device, configuration management is a security control. Separating config from code is a basic SPDF practice that the FDA expects.
- Create a new file called config.py in the same ~/meddevice-security directory.
- Copy the following code into config.py:
"""
Application configuration for the Wound Assessment API.
In production, sensitive values would come from environment variables
or a secrets manager, never hardcoded.
"""
import os
class Config:
"""Base configuration."""
API_VERSION = "1.0.0"
MODEL_VERSION = "deepview-sim-v2"
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-in-production")
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
What does this code do?
The Config class holds all application settings in one place.
- API_VERSION and MODEL_VERSION track which version of the software and AI model are running. These appear in your SBOM and threat model later.
- SECRET_KEY pulls from an environment variable with a dev-only fallback. In production, this would never be hardcoded.
- MAX_CONTENT_LENGTH limits upload size to 16 MB. This is a security control that prevents denial-of-service attacks via oversized requests.
- Save config.py.
- Confirm both files exist by running:
ls
You should see app.py, config.py, requirements.txt, and venv in the output.
Missing one of the files?
- Make sure you saved both files inside ~/meddevice-security and not inside the venv folder or your home directory.
- Double check that the filenames are lowercase: app.py and config.py.
help me find where my files were saved
✔️ Awesome, I've got everything!
Great. Make sure config.py is saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete config.py file for comparison:
"""
Application configuration for the Wound Assessment API.
In production, sensitive values would come from environment variables
or a secrets manager, never hardcoded.
"""
import os
class Config:
"""Base configuration."""
API_VERSION = "1.0.0"
MODEL_VERSION = "deepview-sim-v2"
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-in-production")
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
Verify the application runs
Every component in this app maps to a real medical device concern. The /assess endpoint handles Protected Health Information (PHI). The cryptography library handles data-at-rest encryption. The Flask web framework exposes network attack surface. These will all appear in your threat model and SBOM.
- Start the Flask application by running:
python app.py
What should I see?
Your terminal should display a message like * Running on http://127.0.0.1:5000 indicating the server is listening. The terminal will remain occupied while the server runs.
- Open a second terminal window.
- Test the health endpoint by running:
curl http://127.0.0.1:5000/health
You should see a JSON response containing "status": "operational" and "service": "wound-assessment-api".
- Return to the first terminal and stop the server with Ctrl+C.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is activated. You should see (venv) at the beginning of your terminal prompt.
- If not, activate it by running source venv/bin/activate from inside ~/meddevice-security and try again.
- Verify Flask is installed by running pip list | grep Flask. If nothing appears, run pip install flask.
help me fix a ModuleNotFoundError in my Flask app
Your medical device simulation is running and responding to requests. Next up, you will create a STRIDE threat model for this application using OWASP Threat Dragon.
Create a STRIDE Threat Model
Your Flask application is running and responding to requests. Now you have a realistic target to assess.
The next step is to think like an attacker. Threat modeling is the first activity the FDA expects in a Secure Product Development Framework (SPDF). You will use OWASP Threat Dragon to build a STRIDE diagram that maps every component of your wound assessment system, identifies threats against each one, and documents mitigations.
In this step, get ready to:
- Create a new threat model in OWASP Threat Dragon with your system's components.
- Identify at least one threat per STRIDE category with mitigations.
- Save the completed threat model as a JSON file in your project directory.
Create a new threat model in Threat Dragon
STRIDE stands for Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege. These six categories map directly to the FDA's security objectives: Authenticity, Authorization, Availability, Confidentiality, and Secure Updatability.
- Launch OWASP Threat Dragon by running this command in your terminal:
~/Threat-Dragon-ng-2.6.0.AppImage &
What does this command do?
The & at the end runs the AppImage in the background, so you can continue using your terminal while Threat Dragon stays open.
The Threat Dragon window opens with a Welcome screen.
- Click **Start Threat Dragon** to access the Welcome screen.
- Click the plus (+) area on the Welcome page, or select File then New Model from the menu bar.
- When prompted to save the model file, navigate to ~/meddevice-security/ and save it as threat-model.json.
- In the model metadata fields, set the title to DeepView Wound Assessment System.
- Add a description: Threat model for a medical wound-imaging device backend that processes patient data and returns AI-driven healing assessments.
Why start with the model title?
In an FDA submission, your threat model is a formal design-controlled document. The title and description let reviewers immediately understand what system this model covers without opening the diagram.
Now you need to create a STRIDE diagram inside this model. This is where you draw the data flow diagram for your wound assessment system.
- Add a new diagram to the model. Name it STRIDE Wound Assessment and set the diagram type to STRIDE.
- Click on the diagram name to open the diagram editor.
You are now in the diagram editor with a blank canvas. Add the following elements from the left toolbar to represent your system architecture:
- Add an Actor element. Name it Clinician. This represents the healthcare provider submitting wound data.
- Add a second Actor element. Name it Patient Record System. This represents the external EHR system.
- Add a Process element. Name it Wound Assessment API. This is your Flask application.
- Add a Store element. Name it Assessment Database. This is where results are persisted.
- Draw a Data Flow from Clinician to Wound Assessment API. Label it Wound Image Metadata.
- Draw a Data Flow from Wound Assessment API back to Clinician. Label it Healing Prediction.
- Draw a Data Flow between Patient Record System and Wound Assessment API. Label it Patient Demographics.
- Add a Trust Boundary box around Wound Assessment API and Assessment Database. This marks the device boundary, separating components you control from external entities.
What is a trust boundary?
A trust boundary marks the line where data crosses from a less-trusted zone (like an external network) into a more-trusted zone (your device). Every time data crosses a trust boundary, it needs validation and security controls.
In your diagram, the clinician and patient record system are outside the boundary. The API and database are inside it. This mirrors how a real hospital network segment isolates medical devices.
Save the threat model by clicking the save icon in the diagram editor toolbar.
Diagram elements not appearing?
Make sure you selected the correct diagram type (STRIDE) when creating the diagram. If you chose a different type, threat categories will be limited. You can delete the diagram and create a new one with the STRIDE type selected.
If the AppImage does not launch, check that you made it executable in Step 1 with chmod +x.
Help me troubleshoot Threat Dragon not launching or displaying elements correctly.
Identify threats using STRIDE categories
With your diagram built, you can now systematically identify threats. Threat Dragon lets you select each element and add threats associated with it.
- Click on the Wound Assessment API process element in your diagram.
- Click New Threat in the lower-right panel of the diagram editor.
- Fill in the threat details for Spoofing:
- Click Apply to save the threat.
Repeat this process for the remaining five STRIDE categories. Add one threat per category:
- Tampering: Select the data flow labeled Wound Image Metadata. Title: Wound image metadata modified in transit. Mitigation: Encrypt data in transit using TLS 1.3 and validate message integrity with HMAC signatures.
- Repudiation: Select the Wound Assessment API process. Title: Clinician denies submitting assessment. Mitigation: Implement cryptographic audit logging with tamper-evident records per IEC 62443-4-2 CR 2.8.
- Information Disclosure: Select the Wound Assessment API process. Title: PHI leaks through API error messages. Mitigation: Return generic error messages to clients and log detailed errors server-side only.
- Denial of Service: Select the Wound Assessment API process. Title: Assessment API overwhelmed by requests. Mitigation: Implement rate limiting, request queuing, and horizontal scaling behind a load balancer.
- Elevation of Privilege: Select the Assessment Database store. Title: Clinician accesses other facility records. Mitigation: Enforce row-level security with facility-scoped access tokens validated on every query.
- For each threat, click New Threat on the selected element, fill in the Title, Type, Description, Mitigation, Severity, and Status fields, then click Apply.
Why map mitigations to IEC 62443?
IEC 62443-4-2 is an FDA-recognized consensus standard for industrial automation and control system security. When your threat model references specific control requirements (like CR 1.1 for authentication), it demonstrates to FDA reviewers that your security design follows established industry standards, not ad hoc decisions.
Use New Threat by Type for faster entry
Threat Dragon offers a New Threat by Type button that cycles through all STRIDE categories for the selected element. Use Previous and Next to navigate through the suggestions, then click Apply on any that match your system.
Save the threat model again after adding all six threats.
Save and export your threat model
Your threat model is already being saved to ~/meddevice-security/threat-model.json because you chose that path when you created the model. Confirm the file exists by switching back to your terminal.
- Confirm the threat model file exists by running:
ls ~/meddevice-security/threat-model.json
You should see the file path printed, confirming the model was saved successfully.
Why JSON format matters for FDA submissions
Threat Dragon saves models in JSON format. This makes your threat model machine-readable, version-controllable with Git, and diffable between versions.
When the FDA asks how your threat model changed after adding a new feature, you can show the exact diff. This is what design-controlled documentation means in practice.
File not found?
If the ls command shows no file, return to Threat Dragon and use File then Save As to manually save. Navigate to ~/meddevice-security/ and name the file threat-model.json.
Help me save my threat model to the correct location.
Your threat model is complete. You now have a formal STRIDE diagram that documents your system's architecture, attack surface, and security controls. Next, you will generate an FDA-compliant Software Bill of Materials from your application's dependencies.
Generate an FDA-Compliant SBOM
Your threat model identifies what could go wrong with your medical device. Now you need to document exactly what software is inside it so that vulnerabilities can be tracked over time.
The FDA requires every medical device manufacturer to provide a Software Bill of Materials (SBOM) in their premarket submissions. You will use Syft to generate SBOMs in both CycloneDX and SPDX formats from your Python project.
In this step, get ready to:
- Generate a CycloneDX SBOM from your Python project using Syft.
- Inspect the SBOM to understand its structure and contents.
- Generate an SPDX SBOM for format comparison.
Generate the CycloneDX SBOM from your project directory
Section 524B of the FD&C Act requires manufacturers to "provide a software bill of materials, including commercial, open-source, and off-the-shelf software components" in premarket submissions. This is a legal requirement for any cyber device that connects to the internet.
The SBOM lets the FDA, hospitals, and security researchers know exactly what software is inside your device. They use it to track vulnerabilities as new CVEs are disclosed over time.
- In your terminal, generate a CycloneDX SBOM from your project directory by running this command:
syft dir:. -o cyclonedx-json=meddevice-sbom.cdx.json
What does this command do?
This command has two key parts:
- dir:. tells Syft to scan the current directory as a filesystem (not a container image). Syft detects your Python packages from the virtual environment, requirements.txt, and any other package manifests.
- -o cyclonedx-json=meddevice-sbom.cdx.json tells Syft to output in CycloneDX JSON format, writing the result to the named file. CycloneDX is an OWASP standard designed for security use cases. It supports vulnerability tracking, license compliance, and component dependency graphs.
- Confirm the file was created by running:
ls meddevice-sbom.cdx.json
You should see meddevice-sbom.cdx.json listed in the output.
Don't see the file?
- Make sure your terminal is in the ~/meddevice-security directory. Run pwd to check.
- Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate.
- Verify Syft is installed by running syft version. If it is not found, revisit the Syft installation in Step 1.
Help me troubleshoot why Syft isn't generating my SBOM file.
Inspect the SBOM contents
The SBOM is a machine-readable JSON file. Inspecting it helps you understand what an FDA reviewer would examine when evaluating your device's software composition.
- View the first 100 lines of your SBOM in a readable format by running:
cat meddevice-sbom.cdx.json | python3 -m json.tool | head -100
What should I see?
Look for these key sections in the output:
- The components array listing every package (Flask, Werkzeug, Jinja2, cryptography, requests, and their transitive dependencies).
- Each component's version, purl (Package URL), and type fields.
- The metadata.tools section showing Syft as the generating tool.
This is exactly what an FDA reviewer would examine. They want to see what open-source components are in this device, whether any are known-vulnerable, and whether each component can be traced to its source.
You should see formatted JSON output with a components array listing your installed packages and their metadata.
Seeing an empty components array?
- Make sure your virtual environment was activated before you ran the syft command. Syft discovers packages from the venv/lib directory and requirements.txt.
- Confirm that requirements.txt exists in your directory. Run cat requirements.txt to check its contents.
Help me figure out why Syft found no components in my project.
Generate an SPDX SBOM for comparison
Different organizations prefer different SBOM formats. CycloneDX is more security-focused with better vulnerability correlation. SPDX originated from the Linux Foundation and is more license-focused. Having both shows you understand the ecosystem.
- Generate an SPDX-format SBOM from the same project directory by running:
syft dir:. -o spdx-json=meddevice-sbom.spdx.json
Why generate both formats?
Both CycloneDX and SPDX are accepted SBOM formats for FDA premarket submissions. CycloneDX supports vulnerability tracking and VEX (Vulnerability Exploitability eXchange). SPDX is stronger for license compliance audits.
In an interview, being able to explain the trade-offs between these two formats demonstrates depth beyond just running the tools.
- Confirm both SBOM files exist by running:
ls meddevice-sbom.*
You should see both meddevice-sbom.cdx.json and meddevice-sbom.spdx.json listed.
Why this workflow matters for FDA compliance
In medical device development, you never ship unpinned dependencies. The SBOM you just generated reflects exactly what is installed in your environment, not what you hope is installed.
This is the artifact an FDA reviewer examines to answer: what open-source components are in this device? Can we trace each component to its source? The SBOM is version-controllable, machine-readable, and diffable between releases.
You now have a complete software inventory for your medical device application. Next up, you will scan this SBOM for known security vulnerabilities using Grype.
Scan for Vulnerabilities with Grype
You have a complete SBOM documenting every software component inside your medical device application. Now comes the question the FDA cares most about in postmarket surveillance: are any of those components vulnerable?
The FDA's postmarket guidance requires manufacturers to "monitor, identify, and address, in a reasonable time, postmarket cybersecurity vulnerabilities and exploits." Grype is the vulnerability scanner that pairs with Syft. It takes your SBOM as input and cross-references every component against vulnerability databases like the NVD, GitHub Security Advisories, and distribution feeds.
In this step, get ready to:
- Run Grype against your CycloneDX SBOM to identify known vulnerabilities.
- Generate a JSON vulnerability report for your cybersecurity documentation.
- Use severity filtering to demonstrate CI/CD pipeline gating.
Run Grype against your SBOM
In production, you generate an SBOM once during the build process and then scan it repeatedly as new vulnerabilities are disclosed. This is faster and more reproducible than rescanning the filesystem each time.
- Run Grype against your CycloneDX SBOM by running this command:
grype sbom:./meddevice-sbom.cdx.json
What does this command do?
The sbom: prefix tells Grype to read a pre-generated SBOM file instead of scanning a filesystem or container image directly.
On first run, Grype downloads its vulnerability database (approximately 200MB). This takes a minute or two depending on your internet connection. After the download, it outputs a table showing any vulnerabilities found in your dependencies.
You should see a table in your terminal with columns for the package name, installed version, vulnerability ID (CVE), severity, and whether a fix is available.
Why scan the SBOM instead of the directory?
Scanning the SBOM means you can run vulnerability checks in CI/CD pipelines where the original source code might not be present. It also ensures you are scanning the exact set of components documented in your FDA submission.
Seeing a database download error?
Grype needs internet access to download its vulnerability database on first run. Make sure your VirtualBox VM has NAT or bridged networking enabled.
If the download stalls, try running the command again. The database is cached at ~/.cache/grype/ after the first successful download.
Help me troubleshoot Grype's vulnerability database download failing in my Ubuntu VM.
Generate a JSON vulnerability report
The terminal table is useful for quick review, but your FDA cybersecurity documentation needs a machine-readable artifact you can store, version-control, and reference in your Cybersecurity Management Plan.
- Generate a full JSON vulnerability report by running this command:
grype sbom:./meddevice-sbom.cdx.json -o json > vulnerability-report.json
What does this command do?
The -o json flag tells Grype to output its findings in structured JSON format instead of the human-readable table. The > vulnerability-report.json redirects that output into a file.
This JSON report is your vulnerability assessment artifact. In a real FDA submission, you would reference it in your Cybersecurity Management Plan alongside your assessment of each vulnerability's exploitability and potential patient harm.
- Inspect the severity findings in your report by running:
cat vulnerability-report.json | python3 -m json.tool | grep -A 5 '"severity"'
- Confirm the file exists by running:
ls vulnerability-report.json
You should see vulnerability-report.json listed in the output. The grep command above should display severity entries like "Critical", "High", "Medium", or "Low" for each finding.
Seeing an empty report or no grep results?
If your dependencies happen to have no known vulnerabilities (rare but possible with very fresh packages), the JSON file will still exist but the matches array will be empty. That is a valid result.
If the file is missing entirely, check that you included the > redirect in your command. Without it, Grype prints JSON to the terminal but does not save it to a file.
Help me figure out why my Grype vulnerability report is empty or missing.
Filter by severity and assess risk
Not every vulnerability requires immediate action. The FDA's guidance distinguishes between "controlled" risks (mitigated or low-impact) and "uncontrolled" risks (critical, unmitigated, requiring action within 60 days).
Grype has a built-in flag that makes it exit with a non-zero code when vulnerabilities of a certain severity are found. This is how you gate a CI/CD pipeline so that vulnerable builds cannot be released.
- Test the CI/CD gating behavior by running this command:
grype sbom:./meddevice-sbom.cdx.json --fail-on critical
What does this command do?
The --fail-on critical flag makes Grype exit with code 2 if any critical vulnerabilities are found. In a real pipeline, a non-zero exit code stops the build and prevents deployment.
If your scan found critical vulnerabilities, you will see the table output followed by Grype returning an error exit code. If no critical vulnerabilities exist, Grype exits cleanly with code 0.
You should see the same vulnerability table as before. If critical vulnerabilities are present, Grype exits with an error. If none are critical, it exits cleanly.
How do I assess risk like the FDA expects?
For each vulnerability you found, consider these questions:
- Is the vulnerable component reachable in your application's execution path?
- Does the vulnerability affect patient safety or data confidentiality?
- Is a fix available, and what is the timeline to patch?
A vulnerability in a library your app never calls at runtime is a "controlled" risk. A vulnerability that could expose patient data through your active API endpoint is "uncontrolled" and requires remediation within 60 days per FDA guidance.
Not sure if a finding is controlled or uncontrolled?
Check the CVE description in your JSON report. If the affected function or feature is not used in your app.py code paths, the risk is likely controlled through non-use. Document this reasoning in your assessment.
Help me assess whether a specific CVE in my medical device dependencies is a controlled or uncontrolled risk.
You now have a complete vulnerability assessment workflow. Your vulnerability-report.json file is the final artifact in your cybersecurity assessment package. Combined with your threat model and SBOM, you have the three core deliverables that FDA reviewers look for in a premarket submission.
Secret mission
Write a Coordinated Vulnerability Disclosure Plan
You have the tools and scans. Now step into the role of a cybersecurity program lead. Create a Coordinated Vulnerability Disclosure plan and remediation timeline that meets FDA Section 524B postmarket requirements. This is the document that separates tool operators from cybersecurity engineers.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally on your Ubuntu VirtualBox, so there are no ongoing costs.
Resources you used:
- Project directory at ~/meddevice-security/ (application code, threat model, SBOMs, vulnerability report, CVD plan, Python virtual environment).
- Grype vulnerability database cached at ~/.cache/grype/ (approximately 200MB).
- Syft binary installed at /usr/local/bin/syft.
- Grype binary installed at /usr/local/bin/grype.
Keep everything running
No action needed. Choose this if you are still actively building or want to reference your cybersecurity assessment artifacts later.
- Your ~/meddevice-security directory contains all your portfolio artifacts (threat model, SBOMs, vulnerability report, CVD plan).
- Consider pushing the directory to a GitHub repository to share with potential employers.
- Syft and Grype remain available for scanning future projects.
- The Grype vulnerability database at ~/.cache/grype/ will stay on disk (approximately 200MB).
Pause - I'll come back to this later
Free up disk space while keeping your project files intact for later use.
- Remove the Grype vulnerability database cache to reclaim approximately 200MB of disk space by running:
rm -rf ~/.cache/grype/
- Your project directory and installed tools (Syft, Grype, Threat Dragon AppImage) remain in place.
- When you return, Grype will re-download its database automatically on the next scan.
Delete - I don't want to use this again
Remove all project resources and tools to start fresh.
- Remove the entire project directory (all code, SBOMs, threat model, reports, and the Python virtual environment) by running:
rm -rf ~/meddevice-security/
- Remove the Grype vulnerability database cache by running:
rm -rf ~/.cache/grype/
- Remove the Syft and Grype binaries by running:
sudo rm /usr/local/bin/syft /usr/local/bin/grype
- After running these commands, all project artifacts and tools are fully removed from your system.
Nice Work!
Nice Work!
Nice work! You have just built a complete medical device cybersecurity assessment package, the exact set of artifacts that FDA requires for premarket submissions under Section 524B.
You've learned how to:
- Build a simulated medical device application representing an AI wound-imaging system, giving you a realistic target for security assessment.
- Create a STRIDE threat model using OWASP Threat Dragon, identifying 6+ threats with mitigations mapped to IEC 62443 controls.
- Generate FDA-compliant CycloneDX and SPDX SBOMs using Syft, fulfilling the Section 524B requirement for documenting every software component in your device.
- Perform vulnerability scanning with Grype, producing a risk-assessed vulnerability report that distinguishes controlled from uncontrolled risks.
- Secret Mission: Author a Coordinated Vulnerability Disclosure plan with FDA-aligned remediation timelines, demonstrating postmarket cybersecurity program readiness.
Ready to quiz yourself?