You're under pressure to produce Software Bills of Materials (SBOMs) for every build that touches federal systems. Three agencies, DoD, NASA, and GSA, have proposed rules requiring SBOMs from contractors, and Executive Order 14028 made transparency mandatory. You don't need a third-party tool to start generating compliant SBOM files today.
This script provides a working SBOM generator that runs in your CI/CD pipeline, produces CycloneDX-format output, and creates the audit trail you'll need when assessors ask how you track component dependencies.
What This Script Does
This Python-based generator creates SBOM files at build time by scanning your project dependencies and outputting a structured JSON document that lists every component, library version, and license in your software artifact. It's designed to run as a pipeline step in GitHub Actions, GitLab CI, or Jenkins.
The script addresses three specific requirements from NIST Secure Software Development Framework guidance:
- Component inventory: Captures all direct and transitive dependencies.
- Build-time validation: Runs before artifact creation, not after deployment.
- Immutable record: Generates timestamped, versioned SBOM files tied to specific commit hashes.
Prerequisites
Before you run this script, ensure you have:
- Python 3.8 or later installed in your build environment.
- Read access to your project's dependency manifest files (requirements.txt, package.json, go.mod, or equivalent).
- Write permissions to your artifact storage location.
- A designated directory structure for SBOM output (we'll create
/sbom-outputin the repository root).
Your CI/CD runner must have network access to pull the CycloneDX Python library. If you're working in an air-gapped environment, pre-stage the library in your internal package repository.
The Script
#!/usr/bin/env python3
"""
SBOM Generator for Federal Compliance
Generates CycloneDX-format SBOM from project dependencies
"""
import json
import subprocess
import hashlib
import os
from datetime import datetime
from pathlib import Path
def generate_component_hash(name, version):
"""Create deterministic component identifier"""
component_string = f"{name}@{version}"
return hashlib.sha256(component_string.encode()).hexdigest()[:16]
def scan_python_dependencies(manifest_path):
"""Extract components from requirements.txt"""
components = []
with open(manifest_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
if '==' in line:
name, version = line.split('==')
components.append({
"type": "library",
"name": name.strip(),
"version": version.strip(),
"purl": f"pkg:pypi/{name.strip()}@{version.strip()}",
"bom-ref": generate_component_hash(name.strip(), version.strip())
})
return components
def get_build_metadata():
"""Capture build context for audit trail"""
try:
commit_hash = subprocess.check_output(
['git', 'rev-parse', 'HEAD'],
text=True
).strip()
commit_date = subprocess.check_output(
['git', 'log', '-1', '--format=%cI'],
text=True
).strip()
branch = subprocess.check_output(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
text=True
).strip()
except subprocess.CalledProcessError:
commit_hash = "unknown"
commit_date = datetime.utcnow().isoformat()
branch = "unknown"
return {
"commit": commit_hash,
"timestamp": commit_date,
"branch": branch,
"build_id": os.getenv('BUILD_ID', 'local')
}
def generate_sbom(project_name, project_version, manifest_path, output_path):
"""Main SBOM generation function"""
components = scan_python_dependencies(manifest_path)
build_meta = get_build_metadata()
sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": f"urn:uuid:{generate_component_hash(project_name, build_meta['commit'])}",
"version": 1,
"metadata": {
"timestamp": datetime.utcnow().isoformat() + "Z",
"tools": [{
"vendor": "Internal",
"name": "SBOM-Generator",
"version": "1.0"
}],
"component": {
"type": "application",
"name": project_name,
"version": project_version,
"bom-ref": generate_component_hash(project_name, project_version)
},
"properties": [
{"name": "build:commit", "value": build_meta['commit']},
{"name": "build:branch", "value": build_meta['branch']},
{"name": "build:timestamp", "value": build_meta['timestamp']},
{"name": "build:id", "value": build_meta['build_id']}
]
},
"components": components
}
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(sbom, f, indent=2)
print(f"SBOM generated: {output_path}")
print(f"Components tracked: {len(components)}")
print(f"Build commit: {build_meta['commit'][:8]}")
return output_path
if __name__ == "__main__":
import sys
if len(sys.argv) < 4:
print("Usage: generate_sbom.py <project_name> <version> <manifest_path>")
sys.exit(1)
project_name = sys.argv[1]
version = sys.argv[2]
manifest = sys.argv[3]
output = f"sbom-output/{project_name}-{version}-sbom.json"
generate_sbom(project_name, version, manifest, output)
How to Customize It
For Node.js projects: Replace scan_python_dependencies() with a function that parses package-lock.json. Change the purl format to pkg:npm/{name}@{version}.
For Go modules: Parse go.mod and go.sum files. Use pkg:golang/{module}@{version} for package URLs.
For multi-language repositories: Call multiple scanner functions and merge the component arrays before generating the final SBOM.
Build system integration: Set these environment variables in your CI/CD platform:
BUILD_ID: Your pipeline's unique build identifier.ARTIFACT_REGISTRY: Where to upload the final SBOM file.
License detection: Add a license scanning function that queries package registries or parses LICENSE files. Insert license data into each component object as "licenses": [{"license": {"id": "MIT"}}].
Vulnerability scoring: If you're implementing the CVSS-scoring validation mentioned in NIST guidance, add a post-generation step that queries the National Vulnerability Database and appends CVE identifiers to affected components.
Validation Steps
After your first pipeline run, verify the SBOM meets basic compliance requirements:
- Format check: Run
cat sbom-output/*.json | python -m json.toolto confirm valid JSON structure. - Component count: Compare the component count in your SBOM against your dependency manifest, they should match within a few entries (accounting for dev-only dependencies you may exclude).
- Commit linkage: Open the SBOM file and confirm the
build:commitproperty matches your current git HEAD. - Artifact storage: Verify the SBOM file uploads to your artifact repository alongside the application binary.
- Retrieval test: Attempt to download the SBOM using only the build ID, assessors will do this during audits.
When DoD, NASA, or GSA assessors review your software delivery process, they'll ask how you generate SBOMs and whether you can produce historical bills of materials for past releases. This script creates both the current SBOM and the audit trail that proves you've tracked dependencies consistently across builds.
Store SBOM files for the retention period specified in your contract (typically three years for DFARS 252.204-7012 records). Tag them with the same version identifiers you use for application releases so you can correlate vulnerability disclosures to specific builds when the next Log4j-scale incident hits your supply chain.



