AI Medical Device SBOM Matrix
Build a CLI tool mapping 35+ SBOM elements across 12 medical device standards.
Introduction
30 Second Summary
When a hospital buys an AI-powered diagnostic tool, regulators need a full inventory of the software inside it. The problem is that more than a dozen different standards each define their own version of what that inventory should include.
In this project, you will build a Python command-line tool that unifies SBOM element definitions from 12 regulatory and standards frameworks into a single queryable reference matrix. You will filter elements by standard, category, or requirement level and export the results as machine-readable JSON.
What You'll Build
Run the finished tool, pick a standard or category, and see a color-coded terminal table showing every SBOM element your AI medical device needs, where each requirement comes from, and whether it is required or optional.
By the end of this project, you'll have:
- A queryable matrix of 35+ SBOM elements spanning CycloneDX 1.7, SPDX 3.0, FDA Section 524B, and 9 more frameworks, filterable by standard, category, or requirement level.
- A color-coded Rich terminal table that highlights Required, Recommended, and Optional elements at a glance, with a JSON export for downstream SBOM generation tools.
- Pydantic schema validation that catches missing or malformed element definitions before they enter the matrix.
- Secret Mission: Add a gap analysis feature that loads a device's SBOM profile and highlights which compliance elements are missing for its applicable standards.
Are there any prerequisites?
You need Python 3.10+ and VS Code installed. Familiarity with medical device regulatory frameworks is helpful but not required to build the tool.
Before We Start
Before diving into code, take a moment to lock in what you are building and why it matters.
Your matrix will bridge SBOM specifications and medical device regulatory frameworks so engineering teams can answer one question: given a device's applicable standards, what SBOM elements are needed and where does each requirement come from?
Set Up the Project and Install Dependencies
Your reference matrix will span 35+ SBOM elements across 12 regulatory and standards frameworks. Before you can define the data model that holds all of this, you need a clean project structure with the right tools in place.
This step sets up a Python project with two libraries. Pydantic enforces schema validation on every element definition, and Rich renders formatted tables in your terminal.
In this step, get ready to:
- Create the project directory, virtual environment, and install dependencies.
- Set up the file structure for the Python package.
- Verify the setup with an import check.
Create the project directory and install dependencies
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir sbom-matrix
cd sbom-matrix
A virtual environment keeps your project's dependencies isolated from other Python projects on your machine. The activation command differs by operating system.
- Create and activate a virtual environment by running the commands for your operating system:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
- Check your terminal prompt. You should see (venv) at the beginning of the line.
Don't see (venv) in your prompt?
- Make sure you ran the activation command (source venv/bin/activate on macOS or venv\Scripts\activate on Windows), not just the python3 -m venv venv line.
- On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and try the activation command again.
- help me activate my Python virtual environment
- Install Pydantic and Rich by running:
pip install pydantic==2.13.4 rich==15.0.0
What do these libraries do?
- pydantic==2.13.4 provides data validation through Python type hints. Every SBOM element you define will be validated against a strict schema before it enters the matrix.
- rich==15.0.0 renders beautifully formatted tables, colored text, and structured output in the terminal. Your matrix views will use Rich tables to display element data.
You should see output confirming both packages were installed along with their dependencies.
Seeing an error during install?
- Make sure (venv) appears in your prompt. If it does not, your virtual environment is not active and pip may be pointing to a different Python installation.
- If you see a network-related error, check your internet connection and try running the install command again.
- help me install Pydantic and Rich in my virtual environment
Create the file structure
Your project uses a Python package called sbom_matrix with five files. Each file handles one responsibility: data models, element definitions, rendering, and the CLI entry point.
- Create the package directory and its files by running the commands for your operating system:
macOS
mkdir sbom_matrix
touch sbom_matrix/__init__.py sbom_matrix/models.py sbom_matrix/data.py sbom_matrix/views.py sbom_matrix/cli.py
Windows
mkdir sbom_matrix
New-Item -ItemType File -Path sbom_matrix\__init__.py
New-Item -ItemType File -Path sbom_matrix\models.py
New-Item -ItemType File -Path sbom_matrix\data.py
New-Item -ItemType File -Path sbom_matrix\views.py
New-Item -ItemType File -Path sbom_matrix\cli.py
- Confirm the files exist by running:
ls sbom_matrix/
You should see five files listed: __init__.py, cli.py, data.py, models.py, and views.py.
What is each file for?
- __init__.py marks the directory as a Python package. It stays empty.
- models.py will hold the Pydantic data models that define the schema for SBOM elements.
- data.py will hold the actual element definitions populated with standards references.
- views.py will handle Rich table rendering for the matrix output.
- cli.py will be the command-line entry point that ties everything together.
Verify the setup
Before you run the verification command, take a moment to predict. If both libraries installed correctly and the virtual environment is active, what do you expect to see?
- Run the import check by running:
python -c "from pydantic import BaseModel; from rich.table import Table; print('Setup OK')"
You should see Setup OK printed in your terminal. This confirms that both Pydantic and Rich are installed and importable inside your virtual environment.
Seeing an import error?
- If you see ModuleNotFoundError, your virtual environment may not be active. Check for (venv) in your prompt, re-activate if needed, and run the pip install command again.
- Make sure you are running the command from inside the sbom-matrix directory. Run pwd (macOS) or cd (Windows) to check your current location.
- help me fix my import error for Pydantic or Rich
Your project structure is ready and both libraries are installed. Next up, you'll define the Pydantic data model that gives every SBOM element a rigorous, validated schema.
Define the SBOM Element Data Model
Your matrix will map SBOM elements across 12 regulatory and standards frameworks. Each element needs a name, a category, references to the standards that require it, and field paths for both CycloneDX and SPDX. Without a rigorous schema, those definitions will drift and become inconsistent as you add more elements.
Pydantic solves this by enforcing that every element has its required fields before it enters the matrix. In this step, you will define the data model in models.py, add filtering methods so the matrix is queryable, and confirm that schema validation catches incomplete definitions.
In this step, get ready to:
- Define the enums and core Pydantic models for SBOM elements, standards references, and device profiles.
- Add filtering, gap analysis, and serialization methods to the matrix class.
- Verify that Pydantic raises a validation error when a required field is missing.
Define the enums and model imports
Every SBOM element in the matrix needs two classification axes: a requirement level (is the standard mandatory or optional?) and a category (does it come from a baseline spec, an FDA rule, or an AI governance framework?). Enums lock down these values so no one can accidentally type a freeform string.
- Open models.py in the sbom_matrix folder and add the imports and enum definitions by pasting this code:
from enum import Enum
from pydantic import BaseModel, Field
# Requirement level for each SBOM element within a given standard
class RequirementLevel(str, Enum):
REQUIRED = "Required"
RECOMMENDED = "Recommended"
OPTIONAL = "Optional"
# Category grouping that maps each element to its regulatory origin
class ElementCategory(str, Enum):
BASELINE = "BASELINE"
FDA_CYBER = "FDA_CYBER"
AI_LIFECYCLE = "AI_LIFECYCLE"
RISK_SAFETY = "RISK_SAFETY"
SOFTWARE_LIFECYCLE = "SOFTWARE_LIFECYCLE"
SECURITY = "SECURITY"
TRANSPARENCY = "TRANSPARENCY"
What does this code do?
RequirementLevel captures whether a standard mandates an element (Required), recommends it, or treats it as optional. Each level maps directly to how SBOM tools flag compliance gaps.
ElementCategory groups elements by their regulatory origin: BASELINE for spec-native fields, FDA_CYBER for Section 524B, AI_LIFECYCLE for PCCP/GMLP/NIST/ISO AI standards, and so on. This lets you filter the matrix by regulatory domain later.
- Save models.py.
- Confirm the enums load by running this command:
python -c "from sbom_matrix.models import RequirementLevel, ElementCategory; print(RequirementLevel.REQUIRED.value, ElementCategory.BASELINE.value)"
You should see Required BASELINE printed in your terminal.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is still activated. You should see (venv) or similar at the start of your terminal prompt.
- Check that you are running the command from inside the sbom-matrix project folder, not from inside the sbom_matrix package folder.
- Confirm that sbom_matrix/__init__.py exists. Python needs this file to treat the folder as a package.
Ask for help: Why am I getting a ModuleNotFoundError when importing from sbom_matrix?
Define the data model classes
Each SBOM element in the matrix links to one or more standards via a StandardReference. The SBOMElement model captures the element's name, description, category, and field paths for both CycloneDX and SPDX. A DeviceProfile model records which standards apply to a specific device, which you will use later for gap analysis.
- In models.py, add the three model classes below the ElementCategory enum by pasting this code:
# Links an element to a specific standard, its clause, and requirement level
class StandardReference(BaseModel):
standard: str
clause: str = ""
requirement_level: RequirementLevel = RequirementLevel.RECOMMENDED
# A single SBOM element with cross-standard field mappings
class SBOMElement(BaseModel):
name: str
description: str
category: ElementCategory
standard_refs: list[StandardReference] = Field(default_factory=list)
cyclonedx_path: str = ""
spdx_path: str = ""
notes: str = ""
# Captures a device's applicable standards and present elements
class DeviceProfile(BaseModel):
device_name: str
applicable_standards: list[str] = Field(default_factory=list)
present_elements: list[str] = Field(default_factory=list)
What do these models represent?
StandardReference ties an element to a specific clause in a standard, like FDA 524B clause 524B(b)(3) at the Required level. One element can have multiple references, connecting it across standards.
SBOMElement is the core building block. Fields like cyclonedx_path and spdx_path map the element to its location in each spec's schema, so teams know exactly where the value lives in a generated BOM.
DeviceProfile records which standards apply to a particular device and which SBOM elements it already includes. You will use this in a later step for gap analysis.
- Save models.py.
- Test that the models instantiate correctly by running:
python -c "from sbom_matrix.models import SBOMElement, ElementCategory; e = SBOMElement(name='Test', description='A test element', category=ElementCategory.BASELINE); print(e.model_dump())"
You should see a dictionary printed with keys like name, description, category, and empty defaults for standard_refs, cyclonedx_path, and spdx_path.
Seeing a NameError or ImportError?
- Make sure you pasted the model classes below the ElementCategory enum, not above it. StandardReference depends on RequirementLevel being defined first.
- Check that the from pydantic import BaseModel, Field import at the top of the file includes Field.
Ask for help: Why am I getting a NameError when defining my Pydantic models in models.py?
Add filtering methods to SBOMMatrix
The SBOMMatrix class holds the full list of elements and provides methods to query them. Filtering by standard answers questions like "What does ISO 14971 require?" Filtering by category answers "Show me all AI lifecycle elements."
- In models.py, add the SBOMMatrix class below the DeviceProfile class by pasting this code:
# The full matrix holding all SBOM elements with query methods
class SBOMMatrix(BaseModel):
elements: list[SBOMElement] = Field(default_factory=list)
# Return elements that reference the given standard (case-insensitive match)
def by_standard(self, name: str) -> list[SBOMElement]:
return [
e for e in self.elements
if any(name.lower() in r.standard.lower() for r in e.standard_refs)
]
# Return elements in the given category
def by_category(self, cat: ElementCategory) -> list[SBOMElement]:
return [e for e in self.elements if e.category == cat]
# Return elements where any standard reference matches the given level
def by_requirement_level(self, level: RequirementLevel) -> list[SBOMElement]:
return [
e for e in self.elements
if any(r.requirement_level == level for r in e.standard_refs)
]
# Collect and return all unique standard names across every element
def all_standards(self) -> list[str]:
standards: set[str] = set()
for e in self.elements:
for r in e.standard_refs:
standards.add(r.standard)
return sorted(standards)
What do these filtering methods do?
- by_standard() does a case-insensitive substring match against each element's standard references. Passing "FDA" returns every element tied to any FDA guidance.
- by_category() returns elements in a single category, like AI_LIFECYCLE for all PCCP, GMLP, NIST, and ISO AI elements.
- by_requirement_level() finds every element that is marked Required (or Recommended or Optional) by at least one standard.
- all_standards() collects every unique standard name in the matrix and returns them sorted, giving you a quick inventory of which frameworks the matrix covers.
- Save models.py.
Add gap analysis and serialization methods
The filtering methods answer "which elements does a standard require." The gap analysis method flips the question: "given my device's applicable standards and the elements I already have, what's missing?" The serialization methods let you export and import the matrix as JSON for downstream tooling.
- Still in models.py, add these methods inside the SBOMMatrix class, directly below the all_standards method. Make sure they are indented at the same level as the other methods:
# Compare a device profile against the matrix and return missing elements
def gap_analysis(
self, profile: DeviceProfile
) -> list[tuple[SBOMElement, StandardReference]]:
relevant = [
e for e in self.elements
if any(
any(std.lower() in r.standard.lower() for r in e.standard_refs)
for std in profile.applicable_standards
)
]
missing: list[tuple[SBOMElement, StandardReference]] = []
for element in relevant:
if element.name not in profile.present_elements:
matching_refs = [
r for r in element.standard_refs
if any(
std.lower() in r.standard.lower()
for std in profile.applicable_standards
)
]
for ref in matching_refs:
missing.append((element, ref))
return missing
# Serialize the full matrix to a JSON string
def to_json(self, indent: int = 2) -> str:
return self.model_dump_json(indent=indent)
# Deserialize a JSON string back into an SBOMMatrix instance
@classmethod
def from_json(cls, json_str: str) -> "SBOMMatrix":
return cls.model_validate_json(json_str)
What do these methods do?
gap_analysis() takes a DeviceProfile and finds every element required by the device's applicable standards that is not already present. It returns each missing element paired with the specific standard reference that requires it.
to_json() uses Pydantic's built-in model_dump_json() to serialize the entire matrix. from_json() uses model_validate_json() to deserialize it back, running full schema validation on the incoming data.
- Save models.py.
Before you run the verification, think about this: what do you expect will happen when Pydantic tries to create an SBOMElement with only a name but no description or category?
- Create a temporary test file called test_model.py in the sbom-matrix project folder (not inside the sbom_matrix package folder) by pasting this code:
from sbom_matrix.models import SBOMElement, ElementCategory
# Test 1: Missing required fields should raise ValidationError
try:
bad = SBOMElement(name="Incomplete Element")
print("ERROR: Should have raised ValidationError")
except Exception as e:
print(f"Validation caught: {type(e).__name__}")
# Test 2: Correct instantiation with all required fields
elem = SBOMElement(
name="Component Name",
description="Name of the software component",
category=ElementCategory.BASELINE,
cyclonedx_path="components[].name",
spdx_path="name",
)
print(elem.model_dump())
- Run the test script:
python test_model.py
The first test prints Validation caught: ValidationError. Pydantic refused to create the element because description and category are required fields with no defaults. The second test prints a full dictionary with all the element's fields populated.
Why does this validation matter?
Every SBOM element that enters the matrix must have a name, a description, and a category at minimum. Without Pydantic enforcing this, you could accidentally add a half-defined element that silently breaks filtered views or JSON exports downstream.
Not seeing the ValidationError?
- If you see ERROR: Should have raised ValidationError, check that description and category are declared in SBOMElement without default values. They should be description: str and category: ElementCategory with no = ... after them.
- Make sure you are running test_model.py from the sbom-matrix project folder, not from inside the sbom_matrix package folder.
Ask for help: My Pydantic model did not raise a ValidationError when I omitted required fields. What could be wrong?
✔️ Awesome, I've got everything!
Great. Double check you have saved models.py and you can move on to the next step.
ⓧ I'd like to double check the full code
Your models.py should contain these sections in order. Scroll up and confirm each one is present:
- Imports (from enum import Enum and from pydantic import BaseModel, Field).
- RequirementLevel enum with REQUIRED, RECOMMENDED, OPTIONAL.
- ElementCategory enum with seven values (BASELINE through TRANSPARENCY).
- StandardReference model with standard, clause, and requirement_level fields.
- SBOMElement model with name, description, category, standard_refs, cyclonedx_path, spdx_path, and notes.
- DeviceProfile model with device_name, applicable_standards, and present_elements.
- SBOMMatrix model with elements list and seven methods: by_standard(), by_category(), by_requirement_level(), all_standards(), gap_analysis(), to_json(), and from_json().
Your data model is locked in. Every SBOM element must now pass Pydantic validation before it enters the matrix. Next up, you will populate the baseline layer with the core elements from CycloneDX 1.7 and SPDX 3.0, and see the matrix render as a formatted table for the first time.
Populate the Baseline SBOM Elements
Your Pydantic data model can validate element definitions, but the matrix itself has no elements yet.
The baseline layer is where CycloneDX 1.7 and SPDX 3.0 converge on core SBOM fields. By rendering this first version of the matrix before adding regulatory complexity, you confirm the structure works end to end.
In this step, get ready to:
- Define the 8 common baseline elements shared by CycloneDX 1.7 and SPDX 3.0.
- Build a Rich table renderer and CLI entry point to display the matrix.
- Add 6 AI-specific baseline elements from the BOM specifications.
Define the common baseline elements
These 8 elements are where CycloneDX 1.7 and SPDX 3.0 overlap on core SBOM fields. Each element carries a CycloneDX field path and an SPDX field path so teams can generate compliant BOMs in either format.
- Open data.py in your sbom_matrix folder and add the imports, function scaffold, and first element by pasting the following code:
from sbom_matrix.models import (
ElementCategory,
RequirementLevel,
SBOMElement,
SBOMMatrix,
StandardReference,
)
def build_matrix() -> SBOMMatrix:
elements: list[SBOMElement] = []
# --- BASELINE: CycloneDX 1.7 + SPDX 3.0 common elements ---
elements.append(SBOMElement(
name="Component Name",
description="Name of the software, model, or dataset component.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].name", requirement_level=RequirementLevel.REQUIRED),
StandardReference(standard="SPDX 3.0", clause="name", requirement_level=RequirementLevel.REQUIRED),
],
cyclonedx_path="components[].name",
spdx_path="name",
))
return SBOMMatrix(elements=elements)
What does this code do?
Every element follows the same SBOMElement pattern you defined in models.py:
- name and description identify the field.
- standard_refs lists which standards require this field and at what level (Required, Recommended, Optional).
- cyclonedx_path and spdx_path map the element to its exact location in each spec's data structure.
The return SBOMMatrix(elements=elements) at the bottom keeps the function runnable while you add more elements above it.
- Save data.py and verify the first element loads by running:
python -c "from sbom_matrix.data import build_matrix; m = build_matrix(); print(len(m.elements), 'element(s) loaded')"
You should see 1 element(s) loaded printed in your terminal.
Seeing a ModuleNotFoundError?
- Make sure you are running the command from the sbom-matrix project root (the folder that contains the sbom_matrix package directory).
- Confirm your virtual environment is still activated. You should see (venv) or similar at the start of your terminal prompt.
- Check that __init__.py exists inside the sbom_matrix folder. Python needs it to recognise the folder as a package.
Still stuck? Help me debug a ModuleNotFoundError when importing from sbom_matrix.data
- Continue building the element list. In data.py, find the return SBOMMatrix(elements=elements) line and add the version and supplier elements above it:
elements.append(SBOMElement(
name="Component Version",
description="Version identifier of the component.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].version", requirement_level=RequirementLevel.REQUIRED),
StandardReference(standard="SPDX 3.0", clause="software_packageVersion", requirement_level=RequirementLevel.REQUIRED),
],
cyclonedx_path="components[].version",
spdx_path="software_packageVersion",
))
elements.append(SBOMElement(
name="Supplier",
description="Organization or entity that supplied the component.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].supplier", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="suppliedBy", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="components[].supplier",
spdx_path="suppliedBy",
))
Why is Supplier marked Recommended instead of Required?
Both CycloneDX 1.7 and SPDX 3.0 treat supplier as recommended rather than required. Some open-source components have no single supplier, so the specs keep this field optional to avoid blocking valid SBOMs.
- Still above the return line, add the hash and license elements:
elements.append(SBOMElement(
name="Component Hash",
description="Cryptographic hash (SHA-256) of the component artifact.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].hashes[]", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="verifiedUsing", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="components[].hashes[]",
spdx_path="verifiedUsing",
))
elements.append(SBOMElement(
name="License",
description="License information for the component.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].licenses[]", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="hasDeclaredLicense / hasConcludedLicense", requirement_level=RequirementLevel.REQUIRED),
],
cyclonedx_path="components[].licenses[]",
spdx_path="hasDeclaredLicense / hasConcludedLicense",
))
Why does License have different requirement levels?
SPDX 3.0 requires every AI package to have both hasDeclaredLicense and hasConcludedLicense relationships. CycloneDX 1.7 treats license as recommended. This divergence is exactly the kind of detail the matrix captures so teams know which spec is stricter.
- Save data.py and verify the element count by running:
python -c "from sbom_matrix.data import build_matrix; m = build_matrix(); print(len(m.elements), 'element(s) loaded')"
You should see 5 element(s) loaded printed.
- Add the component type element above the return line. This one uses the notes field to capture a spec divergence:
elements.append(SBOMElement(
name="Component Type",
description="Classification of the component (application, library, machine-learning-model, data).",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].type", requirement_level=RequirementLevel.REQUIRED),
StandardReference(standard="SPDX 3.0", clause="software_primaryPurpose", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="components[].type",
spdx_path="software_primaryPurpose",
notes="CycloneDX supports 'machine-learning-model' and 'data' as component types. SPDX uses 'model' as primaryPurpose.",
))
What is the notes field for?
Some elements have important nuances that do not fit cleanly into the structured fields. The notes field captures these. Here, it records that CycloneDX and SPDX use different type labels for ML models.
- Add the last two common baseline elements, dependencies and authorship, above the return line:
elements.append(SBOMElement(
name="Dependencies / Relationships",
description="Dependency relationships between components.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="dependencies[]", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="Relationship", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="dependencies[]",
spdx_path="Relationship",
))
elements.append(SBOMElement(
name="SBOM Author / Creation Info",
description="Identity of the SBOM creator and creation timestamp.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="metadata.authors / metadata.timestamp", requirement_level=RequirementLevel.REQUIRED),
StandardReference(standard="SPDX 3.0", clause="creationInfo (created, createdBy)", requirement_level=RequirementLevel.REQUIRED),
],
cyclonedx_path="metadata.authors / metadata.timestamp",
spdx_path="creationInfo",
))
What do these two elements capture?
- Dependencies / Relationships maps how components connect. CycloneDX uses a flat dependencies[] array. SPDX uses typed Relationship objects.
- SBOM Author / Creation Info records who generated the SBOM and when. Both specs require this so downstream consumers can assess freshness and accountability.
- Save data.py and verify all 8 common elements load by running:
python -c "from sbom_matrix.data import build_matrix; m = build_matrix(); print(len(m.elements), 'element(s) loaded')"
You should see 8 element(s) loaded printed.
Build the table renderer and entry point
Before adding more elements, let's build the rendering so you can see the matrix as a formatted table. Rich's Table class handles column layout, color coding, and line drawing.
- Open views.py in your sbom_matrix folder and add the imports, function signature, and column definitions:
from rich.console import Console
from rich.table import Table
from sbom_matrix.models import RequirementLevel, SBOMElement, StandardReference
def render_matrix_table(
elements: list[SBOMElement], title: str = "AI Medical Device SBOM Reference Matrix"
) -> None:
console = Console()
table = Table(title=title, show_lines=True)
table.add_column("Element", style="bold cyan", max_width=30)
table.add_column("Category", max_width=18)
table.add_column("Standards", max_width=35)
table.add_column("CycloneDX Path", max_width=30)
table.add_column("SPDX Path", max_width=30)
table.add_column("Req Level", max_width=12)
What does this set up?
- show_lines=True draws horizontal lines between every row, making the dense matrix easier to scan.
- style="bold cyan" on the Element column highlights element names in cyan so they stand out.
- max_width on each column prevents long values from blowing out the table width in narrow terminals.
- Complete the function body by adding the row population loop and summary below the column definitions:
for element in elements:
standards_str = ", ".join(r.standard for r in element.standard_refs)
levels = {r.requirement_level for r in element.standard_refs}
if RequirementLevel.REQUIRED in levels:
level_display = "[green]Required[/green]"
elif RequirementLevel.RECOMMENDED in levels:
level_display = "[yellow]Recommended[/yellow]"
else:
level_display = "[dim]Optional[/dim]"
table.add_row(
element.name,
element.category.value,
standards_str,
element.cyclonedx_path or "-",
element.spdx_path or "-",
level_display,
)
console.print(table)
category_counts: dict[str, int] = {}
for e in elements:
category_counts[e.category.value] = category_counts.get(e.category.value, 0) + 1
summary_parts = [f"{cat}: {count}" for cat, count in sorted(category_counts.items())]
console.print(
f"\n[bold]Total elements: {len(elements)}[/bold] | {' | '.join(summary_parts)}"
)
What does this code do?
- The for loop iterates every element and adds a row to the table. It joins all standard names into a comma-separated string for the Standards column.
- The if/elif/else block picks the highest requirement level across all of an element's standard references. Required (green) takes priority over Recommended (yellow), which takes priority over Optional (dim).
- After printing the table, the summary counts how many elements fall into each category and prints a one-line breakdown.
- Save views.py and verify it loads by running:
python -c "from sbom_matrix.views import render_matrix_table; print('Views loaded')"
You should see Views loaded printed with no errors.
✔️ Awesome, I've got everything!
Great. Make sure you have saved views.py before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your views.py contains, in order:
- The three import lines (Console, Table, and the models import).
- The render_matrix_table function signature with the title default.
- Six table.add_column calls (Element, Category, Standards, CycloneDX Path, SPDX Path, Req Level).
- The for element in elements loop with the color-coded requirement level logic.
- The console.print(table) call followed by the category counts summary.
- Open cli.py in your sbom_matrix folder and add the minimal entry point:
import argparse
from sbom_matrix.data import build_matrix
from sbom_matrix.views import render_matrix_table
def main() -> None:
matrix = build_matrix()
render_matrix_table(matrix.elements)
if __name__ == "__main__":
main()
What does this code do?
main() calls build_matrix() to load all element definitions, then passes them to render_matrix_table() for display. The if __name__ == "__main__" guard lets you run the file as a module with python -m sbom_matrix.cli.
The argparse import is unused for now. You will expand it in Step 5 to support filtered views and export.
- Save cli.py.
Before you run the CLI, what do you expect to see? Think about how many elements you defined and what the table columns will contain.
- Run the CLI to see your first matrix table:
python -m sbom_matrix.cli
You should see a formatted Rich table with 8 rows, one per baseline element, showing each element's name, category (all BASELINE), source standards, CycloneDX path, SPDX path, and color-coded requirement level. A summary line below the table reads Total elements: 8.
Not seeing a table?
- If you see No module named sbom_matrix, make sure you are running the command from the sbom-matrix project root directory.
- If you see a SyntaxError, check for missing commas between SBOMElement fields or unmatched parentheses at the end of each elements.append(...) call.
- If the table appears but has fewer than 8 rows, confirm that every element append sits above the return SBOMMatrix(elements=elements) line.
Still stuck? Help me debug why python -m sbom_matrix.cli is not showing a Rich table
✔️ Awesome, I've got everything!
Great. Make sure you have saved cli.py before moving on.
ⓧ I'd like to double check the full code
Your cli.py should contain exactly this:
import argparse
from sbom_matrix.data import build_matrix
from sbom_matrix.views import render_matrix_table
def main() -> None:
matrix = build_matrix()
render_matrix_table(matrix.elements)
if __name__ == "__main__":
main()
Add the AI-specific baseline elements
CycloneDX 1.7 and SPDX 3.0 also define fields specific to ML models and datasets. These still belong to the BASELINE category because they come from the BOM specifications themselves, not from regulatory guidance.
- In data.py, find the return SBOMMatrix(elements=elements) line and add the model card and dataset elements above it:
# --- BASELINE: AI-specific elements from BOM specs ---
elements.append(SBOMElement(
name="Model Card Metadata",
description="Structured model card with intended use, limitations, performance metrics, and ethical considerations.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[].modelCard", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="AI Profile (typeOfModel, informationAboutTraining)", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="components[].modelCard",
spdx_path="ai_AIPackage fields",
))
elements.append(SBOMElement(
name="Dataset Component",
description="Description of datasets used for training, tuning, or testing.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="components[] type=data", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="DatasetPackage", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="components[] (type=data)",
spdx_path="DatasetPackage",
))
What are these AI-specific fields?
- Model Card Metadata is a structured record of a model's intended use, limitations, and performance. CycloneDX nests it under modelCard. SPDX distributes it across the AI Profile fields.
- Dataset Component captures the datasets used for training and testing. CycloneDX treats them as components of type data. SPDX provides the DatasetPackage class.
- Add the training data and architecture elements below the ones you just pasted, still above the return line:
elements.append(SBOMElement(
name="Training Data Reference",
description="Reference to training data sources including provenance and composition.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="compositions / pedigree.ancestors", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="Relationship (trainedOn)", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="compositions / pedigree",
spdx_path="Relationship (trainedOn)",
))
elements.append(SBOMElement(
name="Model Architecture / Task",
description="Architecture family and task type of the ML model.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="modelCard.modelParameters.architectureFamily / task", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="ai_typeOfModel", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="modelCard.modelParameters",
spdx_path="ai_typeOfModel",
))
- Save data.py and check the growing element count by running:
python -m sbom_matrix.cli
You should see a table with 12 rows now. Four new AI-specific elements appear at the bottom.
- Add the last two AI baseline elements, intended use and ethical considerations, above the return line:
elements.append(SBOMElement(
name="Intended Use",
description="Documented intended use cases for the model.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="modelCard.considerations.useCases", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="ai_informationAboutApplication", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="modelCard.considerations.useCases",
spdx_path="ai_informationAboutApplication",
))
elements.append(SBOMElement(
name="Ethical Considerations",
description="Documented ethical considerations and bias information.",
category=ElementCategory.BASELINE,
standard_refs=[
StandardReference(standard="CycloneDX 1.7", clause="modelCard.considerations.ethicalConsiderations", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="SPDX 3.0", clause="ai_knownBias / ai_limitation", requirement_level=RequirementLevel.RECOMMENDED),
],
cyclonedx_path="modelCard.considerations.ethicalConsiderations",
spdx_path="ai_knownBias / ai_limitation",
))
Why are Intended Use and Ethical Considerations separate from the Model Card?
The model card captures the overall metadata structure. Intended Use and Ethical Considerations are called out as separate matrix elements because regulatory bodies (FDA, GMLP) reference them independently. In later steps, you will add regulatory standard references to these same concepts, and having them as distinct elements makes the cross-referencing visible.
- Save data.py.
Before you run the CLI, predict how many rows you expect in the table.
- Run the CLI to see the complete baseline matrix:
python -m sbom_matrix.cli
You should see a formatted Rich table with 14 rows spanning all the baseline elements. The summary line reads Total elements: 14 with BASELINE: 14 in the breakdown.
Seeing fewer than 14 rows?
- Open data.py and check that every elements.append(...) call appears above the return SBOMMatrix(elements=elements) line.
- Look for missing closing parentheses. Each element definition ends with )) (one for SBOMElement(), one for .append()).
- If Python raises a SyntaxError, check for missing commas after field values inside the SBOMElement constructor.
Need help? Help me figure out why my baseline matrix has the wrong number of elements
✔️ Awesome, I've got everything!
Great. Confirm you have saved data.py with all 14 elements.
ⓧ I'd like to double check the full code
Scroll up and confirm your data.py contains all of the following, in order:
- Imports from sbom_matrix.models (ElementCategory, RequirementLevel, SBOMElement, SBOMMatrix, StandardReference).
- The build_matrix() function starting with elements: list[SBOMElement] = [].
- 8 common baseline elements: Component Name, Component Version, Supplier, Component Hash, License, Component Type, Dependencies / Relationships, SBOM Author / Creation Info.
- 6 AI-specific baseline elements: Model Card Metadata, Dataset Component, Training Data Reference, Model Architecture / Task, Intended Use, Ethical Considerations.
- The return SBOMMatrix(elements=elements) line at the end of the function.
Your baseline matrix is live. Next up, you will layer on the FDA regulatory anchor and AI governance standards that transform this generic SBOM reference into a medical device compliance tool.
Layer the FDA Regulatory Anchor and AI Standards
Your matrix currently covers the BOM specification layer. An engineering team looking at it would see component names, versions, hashes, and model card fields. For generic open-source SBOM compliance, that is sufficient.
Medical devices live under a different set of expectations. The FDA requires cybersecurity SBOM fields that neither CycloneDX nor SPDX mandate on their own. AI lifecycle governance standards introduce elements that don't exist in BOM specs at all. This step layers those regulatory and AI governance dimensions onto the matrix.
In this step, get ready to:
- Add FDA Section 524B cybersecurity elements to the matrix.
- Add AI lifecycle elements from PCCP, GMLP, NIST AI RMF, ISO 42001, and ISO/IEC 23894.
- Add risk, safety, security, and transparency elements and wire up standard filtering in the CLI.
Add the FDA Section 524B cybersecurity elements
Section 524B of the FD&C Act requires medical device manufacturers to include a machine-readable SBOM with premarket submissions. The FDA's 2025 cybersecurity guidance goes further, specifying additional SBOM fields beyond what the BOM specifications themselves define.
- In data.py, find the last baseline element (Ethical Considerations) and add the first three FDA cybersecurity elements below it by copying this code:
# --- FDA_CYBER: Section 524B elements ---
elements.append(SBOMElement(
name="Software Support Status",
description="Whether the software component is actively maintained, no longer maintained, or abandoned.",
category=ElementCategory.FDA_CYBER,
standard_refs=[
StandardReference(standard="FDA 524B", clause="Cybersecurity Guidance 2025, SBOM addendum", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="End-of-Support Date",
description="Date when the component manufacturer will cease support.",
category=ElementCategory.FDA_CYBER,
standard_refs=[
StandardReference(standard="FDA 524B", clause="Cybersecurity Guidance 2025, SBOM addendum", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Known Vulnerability References",
description="Known vulnerabilities including CISA KEV Catalog entries.",
category=ElementCategory.FDA_CYBER,
standard_refs=[
StandardReference(standard="FDA 524B", clause="Cybersecurity Guidance 2025, Sec V", requirement_level=RequirementLevel.REQUIRED),
],
))
What does this code do?
Each element uses the FDA_CYBER category and references FDA 524B as its source standard. Unlike the baseline elements, these have no CycloneDX or SPDX field path because they are regulatory requirements, not BOM spec fields.
The CISA KEV Catalog reference in Known Vulnerability References is specific to FDA's cybersecurity guidance. It requires manufacturers to cross-check components against the Known Exploited Vulnerabilities catalog maintained by CISA.
- Add the remaining two FDA cybersecurity elements directly below the code you just pasted:
elements.append(SBOMElement(
name="Vulnerability Discovery Method",
description="How vulnerabilities were discovered (to demonstrate assessment robustness).",
category=ElementCategory.FDA_CYBER,
standard_refs=[
StandardReference(standard="FDA 524B", clause="Cybersecurity Guidance 2025, Sec V", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="SBOM Machine-Readability",
description="SBOM must be in machine-readable format per NTIA baseline attributes.",
category=ElementCategory.FDA_CYBER,
standard_refs=[
StandardReference(standard="FDA 524B", clause="524B(b)(3), NTIA SBOM Baseline", requirement_level=RequirementLevel.REQUIRED),
],
))
Why is SBOM Machine-Readability its own element?
Section 524B(b)(3) explicitly requires the SBOM to be machine-readable, referencing the NTIA baseline attributes. This is a format constraint on the SBOM itself, not a field within a component. Including it as an element makes the requirement traceable.
- Save data.py.
- Run the CLI to confirm the new category appears by running:
python -m sbom_matrix.cli
The summary line at the bottom now shows two categories: BASELINE: 14 and FDA_CYBER: 5. All five FDA elements display Required in green.
Seeing a syntax error?
- Check that each elements.append(SBOMElement( call ends with )). A missing closing parenthesis is the most common cause.
- Make sure the new code goes after the last baseline element and before the return SBOMMatrix(elements=elements) line.
- Verify that the standard_refs list has a trailing comma after the StandardReference(...) entry.
Help me debug a syntax error in my data.py SBOM element definitions.
Add the AI lifecycle elements
Five standards contribute AI lifecycle elements to the matrix. The FDA's PCCP guidance defines how AI device modifications must be documented. GMLP, NIST AI RMF, ISO 42001, and ISO/IEC 23894 each address a different governance dimension.
- In data.py, add the four PCCP-derived elements below the FDA_CYBER section:
# --- AI_LIFECYCLE: PCCP-derived elements ---
elements.append(SBOMElement(
name="Modification Type",
description="Whether modification is automatic or manual, global or local.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="FDA PCCP", clause="Modification Description", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Modification Boundary",
description="Scope of modifications within the device's intended use.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="FDA PCCP", clause="PCCP Guiding Principle 1: Focused and Bounded", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Validation Criteria",
description="Predefined acceptance criteria for verifying and validating modifications.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="FDA PCCP", clause="Modification Protocol", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Performance Thresholds",
description="Quantitative AI-DSF performance specifications and acceptance criteria.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="FDA PCCP", clause="Modification Type 1: Quantitative AI-DSF performance", requirement_level=RequirementLevel.REQUIRED),
],
))
What is PCCP?
A Predetermined Change Control Plan lets manufacturers describe AI model modifications they intend to make after market clearance. The PCCP final guidance (December 2024) expanded scope from ML-DSF to all AI-DSF. Each modification must be classified by type, bounded in scope, and validated against predefined criteria.
- Save data.py and run the CLI:
python -m sbom_matrix.cli
The summary now shows AI_LIFECYCLE: 4 alongside the baseline and FDA categories. All four PCCP elements are marked Required.
- Add the three GMLP elements below the PCCP section:
# --- AI_LIFECYCLE: GMLP-derived elements ---
elements.append(SBOMElement(
name="Intended Purpose",
description="Clearly documented intended use and indications for use.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="GMLP", clause="Principle 9: Users Provided Clear Information", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Training/Test Dataset Independence",
description="Evidence that training and test datasets are independent and representative.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="GMLP", clause="Principle 3: Clinical Study Participants Representative", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Human-AI Role",
description="Definition of the human-AI team interaction and decision boundaries.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="GMLP", clause="Principle 7: Human-AI Team Performance Focus", requirement_level=RequirementLevel.RECOMMENDED),
],
))
What is GMLP?
Good Machine Learning Practice is a set of 10 guiding principles jointly published by the FDA, Health Canada, and MHRA. These principles cover the full AI lifecycle from data quality to post-market monitoring.
- Add the three NIST AI RMF elements below the GMLP section:
# --- AI_LIFECYCLE: NIST AI RMF elements ---
elements.append(SBOMElement(
name="Validity and Reliability Status",
description="Assessment of AI system validity and reliability per TEVV processes.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="NIST AI RMF 1.0", clause="MEASURE 2.6: Safety risks evaluation", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Explainability Approach",
description="Documentation of how AI model outputs are explained and interpreted.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="NIST AI RMF 1.0", clause="MEASURE 2.9: Model explained, validated, documented", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Bias Assessment Status",
description="Results of fairness and bias evaluation.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="NIST AI RMF 1.0", clause="MEASURE 2.11: Fairness and bias evaluated", requirement_level=RequirementLevel.RECOMMENDED),
],
))
What is NIST AI RMF?
The NIST AI Risk Management Framework (AI 100-1) organizes AI governance into four core functions: Govern, Map, Measure, and Manage. The MEASURE subcategories (2.6 through 2.13) define specific evaluation criteria. The matrix captures safety, explainability, and bias assessment from MEASURE.
- Save data.py and run the CLI to confirm the element count is growing:
python -m sbom_matrix.cli
The summary now shows AI_LIFECYCLE: 10. You can see PCCP, GMLP, and NIST AI RMF standards listed in the Standards column.
- Add the three ISO 42001 elements below the NIST AI RMF section:
# --- AI_LIFECYCLE: ISO 42001 elements ---
elements.append(SBOMElement(
name="AI Governance Owner",
description="Designated owner responsible for AI governance within the AIMS.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 42001:2023", clause="Clause 5: Leadership and roles", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Risk Assessment Status (AI)",
description="Status of the AI risk assessment per the AIMS PDCA cycle.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 42001:2023", clause="Clause 6.1.2: AI risk assessment process", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Lifecycle Review Status",
description="Status of AI system lifecycle review and continual improvement.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 42001:2023", clause="Clause 9: Performance evaluation", requirement_level=RequirementLevel.REQUIRED),
],
))
How do ISO 42001 and ISO/IEC 23894 relate?
ISO/IEC 42001:2023 is the certifiable AI management system standard. It defines what governance structures to build. ISO/IEC 23894:2023 provides the risk management methodology. Organizations implement 42001 and embed 23894 as their risk methodology.
- Add the three ISO/IEC 23894 elements below the ISO 42001 section:
# --- AI_LIFECYCLE: ISO/IEC 23894 elements ---
elements.append(SBOMElement(
name="AI Risk ID",
description="Identifier for AI-specific risks (bias, robustness, explainability gaps, misuse).",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 23894:2023", clause="Clause 6.4.2: Risk identification", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="AI Risk Level",
description="Assessed likelihood and consequence of the identified AI risk.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 23894:2023", clause="Clause 6.4.3: Risk analysis", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Treatment Status",
description="Status of risk treatment implementation for the identified AI risk.",
category=ElementCategory.AI_LIFECYCLE,
standard_refs=[
StandardReference(standard="ISO/IEC 23894:2023", clause="Clause 6.5: Risk treatment", requirement_level=RequirementLevel.RECOMMENDED),
],
))
- Save data.py and run the CLI:
python -m sbom_matrix.cli
The summary now shows AI_LIFECYCLE: 16. The AI lifecycle category alone is larger than the entire baseline. That reflects how much governance overhead AI-enabled devices carry.
Add the remaining framework elements and wire up standard filtering
Four more standards complete the matrix. ISO 14971 covers risk management, IEC 62304 covers the software lifecycle, AAMI TIR57 covers security risk, and the FDA transparency guidance covers explainability for ML-enabled devices.
- In data.py, add the three ISO 14971 risk-safety elements below the ISO/IEC 23894 section:
# --- RISK_SAFETY: ISO 14971 elements ---
elements.append(SBOMElement(
name="Hazard Link",
description="Link between the software/AI component and identified device hazards.",
category=ElementCategory.RISK_SAFETY,
standard_refs=[
StandardReference(standard="ISO 14971:2019", clause="Clause 5: Risk analysis - hazard identification", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Risk Control Reference",
description="Reference to risk control measures applied to mitigate identified risks.",
category=ElementCategory.RISK_SAFETY,
standard_refs=[
StandardReference(standard="ISO 14971:2019", clause="Clause 7: Risk control", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Residual Risk Status",
description="Status of residual risk after control measures are applied.",
category=ElementCategory.RISK_SAFETY,
standard_refs=[
StandardReference(standard="ISO 14971:2019", clause="Clause 8: Evaluation of overall residual risk", requirement_level=RequirementLevel.REQUIRED),
],
))
What does ISO 14971 cover?
ISO 14971:2019 is the foundational risk management standard for medical devices. It requires manufacturers to identify hazards, estimate and evaluate risks, apply controls, and assess residual risk throughout the device lifecycle. Every software component in an AI-enabled device needs a traceable link to the device's hazard analysis.
- Add the three IEC 62304 software lifecycle elements:
# --- SOFTWARE_LIFECYCLE: IEC 62304 elements ---
elements.append(SBOMElement(
name="Software Safety Class",
description="Safety classification of the software item (Class A, B, or C).",
category=ElementCategory.SOFTWARE_LIFECYCLE,
standard_refs=[
StandardReference(standard="IEC 62304:2006+AMD1:2015", clause="Clause 4.3: Software safety classification", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Lifecycle Status",
description="Current lifecycle phase of the software (development, maintenance, retired).",
category=ElementCategory.SOFTWARE_LIFECYCLE,
standard_refs=[
StandardReference(standard="IEC 62304:2006+AMD1:2015", clause="Clause 5/6: Development/Maintenance process", requirement_level=RequirementLevel.REQUIRED),
],
))
elements.append(SBOMElement(
name="Configuration Item ID",
description="Unique identifier for the software configuration item under configuration management.",
category=ElementCategory.SOFTWARE_LIFECYCLE,
standard_refs=[
StandardReference(standard="IEC 62304:2006+AMD1:2015", clause="Clause 8: Software configuration management", requirement_level=RequirementLevel.REQUIRED),
],
))
What are IEC 62304 safety classes?
IEC 62304 classifies medical device software into three safety classes. Class A means the software cannot contribute to a hazardous situation. Class B means it can contribute but external controls reduce the risk. Class C means it can directly contribute to death or serious injury. The classification drives how rigorous the development and maintenance processes must be.
- Save data.py and run the CLI to see the new categories:
python -m sbom_matrix.cli
The summary now shows RISK_SAFETY: 3 and SOFTWARE_LIFECYCLE: 3. Five of seven categories are now populated.
- Add the three AAMI TIR57 security elements:
# --- SECURITY: AAMI TIR57 elements ---
elements.append(SBOMElement(
name="Security Risk ID",
description="Identifier for security risk within the device threat model.",
category=ElementCategory.SECURITY,
standard_refs=[
StandardReference(standard="AAMI TIR57:2016/(R)2023", clause="Security risk management process", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Threat / Vulnerability Reference",
description="Reference to specific threat scenario or vulnerability (via STRIDE or equivalent).",
category=ElementCategory.SECURITY,
standard_refs=[
StandardReference(standard="AAMI TIR57:2016/(R)2023", clause="Threat modeling and vulnerability identification", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Security Control",
description="Reference to the security control measure mitigating the identified threat.",
category=ElementCategory.SECURITY,
standard_refs=[
StandardReference(standard="AAMI TIR57:2016/(R)2023", clause="Risk control and verification", requirement_level=RequirementLevel.RECOMMENDED),
],
))
What is AAMI TIR57?
AAMI TIR57 is a Technical Information Report, not a mandatory standard. The FDA recognizes it as the framework for security risk management aligned with ISO 14971. It provides the STRIDE-based threat modeling methodology that connects cybersecurity threats to safety risks.
- Add the two FDA transparency elements to complete the data layer:
# --- TRANSPARENCY: FDA transparency guidance elements ---
elements.append(SBOMElement(
name="Transparency Limitations",
description="Documented limitations of model transparency and explainability.",
category=ElementCategory.TRANSPARENCY,
standard_refs=[
StandardReference(standard="FDA Transparency Guidance 2024", clause="Guiding Principles for Transparency of MLMDs", requirement_level=RequirementLevel.RECOMMENDED),
],
))
elements.append(SBOMElement(
name="Monitoring Status",
description="Status of real-world performance monitoring for the deployed AI model.",
category=ElementCategory.TRANSPARENCY,
standard_refs=[
StandardReference(standard="FDA Transparency Guidance 2024", clause="Lifecycle transparency", requirement_level=RequirementLevel.RECOMMENDED),
StandardReference(standard="GMLP", clause="Principle 10: Deployed models monitored", requirement_level=RequirementLevel.RECOMMENDED),
],
))
Why does Monitoring Status reference two standards?
Post-market monitoring is required by both the FDA Transparency Guidance (June 2024) and GMLP Principle 10. When an element is mandated by multiple standards, the matrix captures all source references so engineering teams know every standard they satisfy with a single field.
- Save data.py and run the CLI:
python -m sbom_matrix.cli
All seven categories are now populated. The summary shows 35+ total elements across BASELINE, FDA_CYBER, AI_LIFECYCLE, RISK_SAFETY, SOFTWARE_LIFECYCLE, SECURITY, and TRANSPARENCY.
The full matrix is now populated, but there is no way to filter it from the command line. Time to add --standard filtering to the CLI so you can query elements by source standard.
- In cli.py, replace the entire contents of the file with the following code:
import argparse
from sbom_matrix.data import build_matrix
from sbom_matrix.models import ElementCategory
from sbom_matrix.views import render_matrix_table
def main() -> None:
parser = argparse.ArgumentParser(description="AI Medical Device SBOM Reference Matrix")
parser.add_argument("--standard", type=str, help="Filter elements by standard name")
args = parser.parse_args()
matrix = build_matrix()
if args.standard:
elements = matrix.by_standard(args.standard)
title = f"Elements for standard: {args.standard}"
else:
elements = matrix.elements
title = "AI Medical Device SBOM Reference Matrix"
render_matrix_table(elements, title)
if __name__ == "__main__":
main()
What does this code do?
- argparse parses command-line arguments. The --standard flag accepts a string to filter by.
- When --standard is provided, the code calls matrix.by_standard() (the filter method you defined in models.py) and sets a descriptive title for the filtered table.
- Without a flag, it renders the full matrix as before.
- Save cli.py.
Before you run the next command, think about what you expect to see. If you filter by FDA 524B, how many elements should appear?
- Test the standard filter by running:
python -m sbom_matrix.cli --standard "FDA 524B"
You should see a filtered table titled Elements for standard: FDA 524B containing exactly 5 elements: Software Support Status, End-of-Support Date, Known Vulnerability References, Vulnerability Discovery Method, and SBOM Machine-Readability. All five show Required in green.
Not seeing filtered results?
- Make sure you wrap the standard name in quotes: "FDA 524B". Without quotes, the shell treats 524B as a separate argument.
- If the table is empty, check that your FDA_CYBER elements use the standard name FDA 524B exactly as written. The by_standard() method does case-insensitive substring matching, but the string must still be present.
Help me debug why my --standard filter returns no results.
✔️ Awesome, I've got everything!
Great. Double check you have saved both data.py and cli.py.
ⓧ I'd like to double check the full code
Your cli.py should match this:
import argparse
from sbom_matrix.data import build_matrix
from sbom_matrix.models import ElementCategory
from sbom_matrix.views import render_matrix_table
def main() -> None:
parser = argparse.ArgumentParser(description="AI Medical Device SBOM Reference Matrix")
parser.add_argument("--standard", type=str, help="Filter elements by standard name")
args = parser.parse_args()
matrix = build_matrix()
if args.standard:
elements = matrix.by_standard(args.standard)
title = f"Elements for standard: {args.standard}"
else:
elements = matrix.elements
title = "AI Medical Device SBOM Reference Matrix"
render_matrix_table(elements, title)
if __name__ == "__main__":
main()
For data.py, scroll up and confirm each of these sections is present in order inside your build_matrix() function, before the return statement:
- 14 BASELINE elements (from the previous step).
- 5 FDA_CYBER elements (Software Support Status through SBOM Machine-Readability).
- 4 PCCP elements, 3 GMLP elements, 3 NIST AI RMF elements, 3 ISO 42001 elements, 3 ISO/IEC 23894 elements (16 AI_LIFECYCLE total).
- 3 RISK_SAFETY elements (Hazard Link, Risk Control Reference, Residual Risk Status).
- 3 SOFTWARE_LIFECYCLE elements (Software Safety Class, Lifecycle Status, Configuration Item ID).
- 3 SECURITY elements (Security Risk ID, Threat / Vulnerability Reference, Security Control).
- 2 TRANSPARENCY elements (Transparency Limitations, Monitoring Status).
Your matrix now spans 12 standards frameworks and 35+ elements. The data is all there, but a flat list of 35+ rows is hard to act on. Next up, you will build filtered views by category and requirement level, add color-coded output, and enable JSON export for downstream tooling.
Build Filtered Views and JSON Export
Your matrix now holds 35+ elements across 7 categories, all viewable in one large table. Scrolling through every element works for a full audit, but it doesn't help when an engineer asks a pointed question like "What does ISO 14971 require?"
In this step, you'll expand the CLI with category, level, and export filters so teams can query the matrix for exactly what they need. You'll also add JSON export for machine-readable consumption by downstream SBOM generation tools.
In this step, get ready to:
- Add --category, --level, --export, and --list-standards arguments to the CLI.
- Wire up filtered views that chain the SBOMMatrix filter methods.
- Export the full or filtered matrix as JSON for downstream tooling.
Replace the CLI imports and argument definitions
The existing cli.py only supports a single --standard filter. You need to expand it with arguments for category, requirement level, JSON export, and a standards listing view.
- In cli.py, replace all existing content with the following imports and argument definitions:
import argparse
import json
import sys
from pathlib import Path
from sbom_matrix.data import build_matrix
from sbom_matrix.models import DeviceProfile, ElementCategory, RequirementLevel
from sbom_matrix.views import render_gap_analysis, render_matrix_table
def main() -> None:
parser = argparse.ArgumentParser(
description="AI Medical Device SBOM Reference Matrix"
)
parser.add_argument(
"--standard", type=str, help="Filter elements by standard name"
)
parser.add_argument(
"--category",
type=str,
choices=[c.value for c in ElementCategory],
help="Filter elements by category",
)
parser.add_argument(
"--level",
type=str,
choices=[l.value for l in RequirementLevel],
help="Filter elements by requirement level",
)
What does this code do?
The imports bring in new modules you'll use in this step. Path handles file paths for JSON export. DeviceProfile and render_gap_analysis are wired up now so the Secret Mission's gap analysis feature works without further edits.
The --category argument uses choices=[c.value for c in ElementCategory] to restrict input to valid category names. argparse will reject any value not in that list automatically.
- Continue adding the remaining argument definitions and the list-standards handler directly below the --level argument:
parser.add_argument(
"--export", type=str, help="Export matrix to JSON file"
)
parser.add_argument(
"--list-standards",
action="store_true",
help="List all standards referenced in the matrix",
)
parser.add_argument(
"--gap-analysis",
type=str,
help="Run gap analysis from a device profile JSON file",
)
args = parser.parse_args()
matrix = build_matrix()
# Handle --list-standards: print all unique standard names
if args.list_standards:
print("Standards referenced in the matrix:")
for std in matrix.all_standards():
print(f" - {std}")
return
What does this code do?
--export takes a file path where the matrix will be written as JSON. --list-standards uses action="store_true" because it's a boolean flag with no value, not a filter.
The list-standards handler calls matrix.all_standards(), which you already defined in models.py. It collects every unique standard name across all elements and returns them sorted.
- Save cli.py.
- Test the list-standards view by running:
python -m sbom_matrix.cli --list-standards
You should see a printed list of 12+ standard names including CycloneDX 1.7, SPDX 3.0, FDA 524B, FDA PCCP, and others.
Seeing an ImportError?
If you see an error about render_gap_analysis, that function doesn't exist in views.py yet. You'll add it later in this step. For now, temporarily comment out the render_gap_analysis import to test the list-standards view, then uncomment it before moving on.
If you see a different error, make sure you pasted both code blocks into cli.py in order and that the indentation is correct.
Help me debug my cli.py import or argument errors.
Add the filter and export logic
The argument parser knows about the new flags, but the main() function doesn't yet do anything with them. You need the filter logic that chains your SBOMMatrix methods and the export handler that writes JSON.
- In cli.py, add the following code directly below the list-standards return statement. This handles the gap analysis argument and applies the filters:
# Handle --gap-analysis: load a device profile and find missing elements
if args.gap_analysis:
profile_path = Path(args.gap_analysis)
if not profile_path.exists():
print(f"Error: Profile file '{args.gap_analysis}' not found.", file=sys.stderr)
sys.exit(1)
profile_data = profile_path.read_text()
profile = DeviceProfile.model_validate_json(profile_data)
gaps = matrix.gap_analysis(profile)
render_gap_analysis(gaps, profile.device_name)
return
# Apply filters based on command-line arguments
elements = matrix.elements
if args.standard:
elements = matrix.by_standard(args.standard)
title = f"Elements for standard: {args.standard}"
elif args.category:
elements = matrix.by_category(ElementCategory(args.category))
title = f"Elements in category: {args.category}"
elif args.level:
elements = matrix.by_requirement_level(RequirementLevel(args.level))
title = f"Elements at level: {args.level}"
else:
title = "AI Medical Device SBOM Reference Matrix"
What does this code do?
The gap-analysis handler reads a JSON file describing a device's profile (applicable standards and present elements), then calls matrix.gap_analysis() to find what's missing. You'll use this in the Secret Mission.
The filter chain uses elif so only one filter applies at a time. Each branch calls the matching SBOMMatrix method you defined in Step 2 and sets a descriptive title for the table.
- Add the export handler and the render call directly below the filter logic. This finishes the main() function:
# Handle --export: write filtered matrix to JSON file
if args.export:
export_matrix = matrix.model_copy()
export_matrix.elements = elements
export_path = Path(args.export)
export_path.write_text(export_matrix.to_json())
print(f"Matrix exported to {args.export} ({len(elements)} elements)")
return
render_matrix_table(elements, title)
if __name__ == "__main__":
main()
What does this code do?
The export handler uses model_copy() to create a shallow copy of the matrix so replacing its elements list with the filtered results doesn't modify the original. It then writes the JSON to the specified file path using Pydantic's built-in to_json() method.
If no --export flag is provided, the function falls through to render_matrix_table() and prints the table to the terminal as usual.
- Save cli.py.
Before you run the category filter, what do you expect it to return? Think about which standards map to the AI_LIFECYCLE category.
- Test the category filter by running:
python -m sbom_matrix.cli --category AI_LIFECYCLE
You should see a filtered table containing only the AI lifecycle elements from FDA PCCP, GMLP, NIST AI RMF 1.0, ISO/IEC 42001:2023, and ISO/IEC 23894:2023. The summary row at the bottom should show the total count for just this category.
Seeing all elements instead of a filtered view?
Make sure you used elif for the args.category branch, not a separate if. A standalone if would re-check after the standard filter and fall through to the else branch.
Help me debug why my --category filter isn't working correctly.
Add the gap analysis renderer to views.py
The CLI already imports render_gap_analysis from views.py, but that function doesn't exist yet. This renderer will power the Secret Mission's gap analysis feature by displaying a table of missing SBOM elements.
- In views.py, add the following function below the render_matrix_table function. Start with the function signature and table column definitions:
def render_gap_analysis(
gaps: list[tuple[SBOMElement, StandardReference]], device_name: str
) -> None:
console = Console()
if not gaps:
console.print(
f"\n[bold green]No gaps found for '{device_name}'. All required elements are present.[/bold green]"
)
return
table = Table(
title=f"Gap Analysis: {device_name} - Missing Elements",
show_lines=True,
)
table.add_column("Missing Element", style="bold red", max_width=30)
table.add_column("Category", max_width=18)
table.add_column("Required By", max_width=30)
table.add_column("Clause", max_width=40)
table.add_column("Req Level", max_width=12)
What does this code do?
The function accepts a list of tuples, where each tuple pairs a missing SBOMElement with the StandardReference that requires it. If no gaps are found, it prints a success message and returns early.
The table columns are tailored for gap analysis: instead of showing CycloneDX/SPDX paths, they show which standard requires the missing element and the specific clause.
- Continue adding the row population and output directly below the column definitions:
for element, ref in gaps:
if ref.requirement_level == RequirementLevel.REQUIRED:
level_display = "[green]Required[/green]"
elif ref.requirement_level == RequirementLevel.RECOMMENDED:
level_display = "[yellow]Recommended[/yellow]"
else:
level_display = "[dim]Optional[/dim]"
table.add_row(
element.name,
element.category.value,
ref.standard,
ref.clause,
level_display,
)
console.print(table)
console.print(f"\n[bold]Total gaps: {len(gaps)}[/bold]")
What does this code do?
The loop iterates over each gap tuple and applies the same color coding you see in the main matrix table: green for Required, yellow for Recommended, dim for Optional. Each row shows the missing element's name, its category, the standard that requires it, and the exact clause.
- Save views.py.
Before you test the JSON export, what format do you expect the output file to be in? Think about which Pydantic method the export handler calls.
- Test the JSON export by running:
python -m sbom_matrix.cli --export matrix.json
You should see a message confirming the export with the total element count. Open matrix.json in VS Code and confirm it contains the full structured matrix with all 35+ elements, their standard references, CycloneDX paths, and SPDX paths.
- Verify the requirement level filter by running:
python -m sbom_matrix.cli --level Required
You should see only elements that have at least one standard reference marked as Required. The requirement level column should show green Required for every row.
Export file is empty or malformed?
Make sure the --export handler appears after the filter logic in cli.py. If it runs before the if args.standard block, the elements variable won't have been filtered yet.
Help me debug my matrix JSON export.
✔️ Awesome, I've got everything!
Great. Double check you've saved both cli.py and views.py.
ⓧ I'd like to double check the full code
For cli.py, scroll up and confirm these sections appear in order:
- Imports (argparse, json, sys, Path, plus DeviceProfile, ElementCategory, RequirementLevel, render_gap_analysis, render_matrix_table).
- Argument definitions: --standard, --category, --level, --export, --list-standards, --gap-analysis.
- List-standards handler with all_standards() loop.
- Gap-analysis handler with DeviceProfile.model_validate_json().
- Filter logic: if args.standard / elif args.category / elif args.level / else.
- Export handler with model_copy() and to_json().
- Final render_matrix_table(elements, title) call and if __name__ == "__main__" block.
For views.py, confirm that render_gap_analysis appears below render_matrix_table and includes: the empty-gaps early return, 5 column definitions (Missing Element, Category, Required By, Clause, Req Level), the for loop with color-coded requirement levels, and the total gaps summary line.
Your matrix is now a queryable CLI tool with filtered views and JSON export. Next up, there's an optional Secret Mission that transforms the matrix from a reference into an actionable compliance checker.
Secret mission
Add Gap Analysis
Your matrix tells you what elements exist across 12 standards frameworks. But which ones are missing for your specific device? In this mission, you will create a hypothetical device profile and run a gap analysis that surfaces every compliance gap.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. There are no cloud resources, no paid services, and no ongoing costs.
Decide whether to keep your project files for future use, or delete them entirely.
Resources you used:
- The sbom-matrix project directory on your Desktop (including all Python source files, the virtual environment, and exported JSON files).
Keep everything running
No action needed. Choose this if you plan to keep using the SBOM reference matrix or extend it with additional standards and elements.
- Your sbom-matrix directory stays on your Desktop with all code and exported JSON files intact.
- The JSON export can be consumed by CI/CD pipelines, SBOM generation tools, or compliance dashboards.
- Pydantic and Rich remain installed inside the project's virtual environment and do not affect the rest of your system.
Pause - I'll come back to this later
Since this project has no running processes or servers, there is nothing to pause. Your files sit on disk and cost nothing.
- If you have a terminal open with the virtual environment active, deactivate it by running:
deactivate
- When you return, reactivate the virtual environment and pick up where you left off.
Delete - I don't want to use this again
Remove the entire project directory to start fresh. This deletes all source files, the virtual environment, and any exported JSON files.
- If your virtual environment is still active, deactivate it first by running:
deactivate
- On macOS or Linux, delete the project directory by running:
cd ~/Desktop
rm -rf sbom-matrix
- On Windows, delete the project directory by running:
cd ~\Desktop
rmdir /s /q sbom-matrix
- Confirm the directory is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see sbom-matrix in the output.
Well Done!
Well Done!
Nice work! You just built a command-line reference matrix that unifies SBOM element definitions across 12 regulatory and standards frameworks into a single queryable data structure.
You've learned how to:
- Build a Pydantic data model that enforces schema validation for SBOM element definitions across 12 regulatory and standards frameworks.
- Populate 35+ minimum SBOM elements spanning CycloneDX 1.7 ML-BOM, SPDX 3.0 AI/Dataset profiles, FDA Section 524B, PCCP, GMLP, NIST AI RMF, ISO 42001, ISO/IEC 23894, ISO 14971, IEC 62304, AAMI TIR57, and FDA transparency guidance.
- Create a CLI tool with filtered views by standard, category, and requirement level, with color-coded Rich table output and JSON export for machine-readable consumption by downstream SBOM tooling.
- Secret Mission: Completed an optional challenge to push your compliance tooling skills further.
Ready to quiz yourself?