Custom SBOM Plugin Examples
Ready-to-use example implementations for lockfile parsers and collectors.
Example 1: Ruby Bundler Gemfile.lock Parser
A complete lockfile parser for Ruby Bundler manifests.
File: .strata/lockfile_parsers/bundler_gemfile.py
"""Parse Ruby Bundler Gemfile.lock files."""
from pathlib import Path
from typing import List
from strata.builders.sbom.lockfile_parsers._base import LockfileParser, RawDependency
class BundlerGemdockParser(LockfileParser):
"""Parse Ruby Bundler's Gemfile.lock format.
Extracts gem names and versions from the BUNDLED WITH section.
Format:
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.0)
actionmail (7.0.0)
...
PLATFORMS
ruby
DEPENDENCIES
rails (~> 7.0.0)
BUNDLED WITH
2.3.0
"""
@property
def ecosystem(self) -> str:
return "gem"
def filename_patterns(self) -> List[str]:
return ["Gemfile.lock"]
def parse(self, path: Path) -> List[RawDependency]:
"""Extract gem dependencies from Gemfile.lock."""
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError as exc:
raise ValueError(f"Could not read {path.name}: {exc}") from exc
deps: List[RawDependency] = []
in_specs = False
for line in lines:
# Skip empty lines and comments
if not line.strip() or line.strip().startswith("#"):
continue
# Enter specs section
if line.strip() == "specs:":
in_specs = True
continue
# Exit specs section
if in_specs and line[0] not in (" ", "\t"):
in_specs = False
# Parse spec line: " gem_name (version)"
if in_specs and line.startswith((" ", "\t")):
spec_line = line.strip()
if not spec_line or "(" not in spec_line:
continue
# Extract gem name and version
name_part, version_part = spec_line.rsplit("(", 1)
name = name_part.strip()
version = version_part.rstrip(")").strip()
if name and version:
deps.append(RawDependency(name=name, version=version))
return deps
Usage:
Drop this file into .strata/lockfile_parsers/bundler_gemfile.py, then run:
strata build sbom -f deploy.yaml
The Ruby gems from Gemfile.lock will be included in the SBOM.
Example 2: Go go.mod Parser
Parse Go module dependencies.
File: .strata/lockfile_parsers/golang_mod.py
"""Parse Go go.mod files."""
from pathlib import Path
from typing import List
from strata.builders.sbom.lockfile_parsers._base import LockfileParser, RawDependency
class GoModParser(LockfileParser):
"""Parse Go go.mod files.
Extracts module dependencies from the require section.
Format:
module github.com/example/myapp
go 1.21
require (
github.com/google/uuid v1.5.0
github.com/sirupsen/logrus v1.9.3
)
require github.com/stretchr/testify v1.8.4 // indirect
"""
@property
def ecosystem(self) -> str:
return "golang"
def filename_patterns(self) -> List[str]:
return ["go.mod"]
def parse(self, path: Path) -> List[RawDependency]:
"""Extract Go module dependencies from go.mod."""
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError as exc:
raise ValueError(f"Could not read {path.name}: {exc}") from exc
deps: List[RawDependency] = []
in_require_block = False
for line in lines:
stripped = line.strip()
# Skip empty lines and comments
if not stripped or stripped.startswith("//"):
continue
# Detect require block start
if stripped == "require (":
in_require_block = True
continue
# Exit require block
if in_require_block and stripped == ")":
in_require_block = False
continue
# Parse single-line require statements
if stripped.startswith("require "):
stripped = stripped[8:].strip()
in_require_block = False
# Parse require lines (inside or outside block)
if stripped and not stripped.startswith("module ") and not stripped.startswith("go "):
# Format: "github.com/user/pkg v1.2.3" or with "// indirect"
parts = stripped.split()
if len(parts) >= 2:
module_name = parts[0]
version = parts[1]
# Strip "// indirect" or similar comments
if version.startswith("//"):
continue
deps.append(RawDependency(name=module_name, version=version))
return deps
Example 3: Composer (PHP) Parser
Parse PHP Composer lock files.
File: .strata/lockfile_parsers/php_composer.py
"""Parse PHP Composer lock files."""
import json
from pathlib import Path
from typing import Any, Dict, List
from strata.builders.sbom.lockfile_parsers._base import LockfileParser, RawDependency
class ComposerLockParser(LockfileParser):
"""Parse PHP Composer composer.lock files.
Extracts package names and versions from the packages array.
"""
@property
def ecosystem(self) -> str:
return "composer"
def filename_patterns(self) -> List[str]:
return ["composer.lock"]
def parse(self, path: Path) -> List[RawDependency]:
"""Extract PHP packages from composer.lock."""
try:
data: Any = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"Failed to parse {path.name}: {exc}") from exc
if not isinstance(data, dict):
raise ValueError(f"Expected JSON object, got {type(data).__name__}")
deps: List[RawDependency] = []
# Parse packages array
for section in ["packages", "packages-dev"]:
packages: List[Dict[str, Any]] = data.get(section) or []
for pkg in packages:
if not isinstance(pkg, dict):
continue
name = pkg.get("name")
version = pkg.get("version")
if name:
deps.append(RawDependency(name=str(name), version=version or None))
return deps
Example 4: Custom Collector — Database Schemas
A collector that extracts database schema versions from infrastructure code.
File: .strata/collectors/database_schemas_collector.py
"""Collect database schema components from infrastructure code."""
from pathlib import Path
from typing import List
from strata.builders.sbom.base_sbom_collector import BaseSbomCollector
from strata.logger import get_logger
from strata.models.platform_artifact_model import PlatformArtifactModel
from strata.models.sbom_model import SbomComponentModel
logger = get_logger(__name__)
class DatabaseSchemasCollector(BaseSbomCollector):
"""Collect database and schema version components.
This is an example collector that scans for database declarations
in platform modules and produces SBOM components for them.
In a real implementation, this might read from:
- Database resource declarations (e.g., Azure SQL, RDS)
- Migration versions
- ORM configuration files
"""
def get_collector_name(self) -> str:
return "database_schemas"
def collect(
self,
platform: PlatformArtifactModel,
work_path: Path,
deployment_build_path: Path,
) -> List[SbomComponentModel]:
"""Extract database components from platform specification."""
self._reset_warnings()
components: List[SbomComponentModel] = []
if not platform.spec or not platform.spec.modules:
return components
seen_databases: set[str] = set()
for module in platform.spec.modules:
# Example: assume modules have a 'databases' list
# In real code, adapt this to your actual schema structure
databases = getattr(module, "databases", None) or []
for db in databases:
db_name = getattr(db, "name", "")
db_engine = getattr(db, "engine", "") # e.g., "postgres", "mysql"
db_version = getattr(db, "version", "")
if not db_name:
continue
purl_key = f"{db_engine}:{db_name}:{db_version}"
if purl_key in seen_databases:
continue
seen_databases.add(purl_key)
# Build a meaningful PURL
purl = f"pkg:generic/{db_engine}/{db_name}@{db_version or 'unknown'}"
components.append(
SbomComponentModel(
component_type="framework",
name=f"{db_name} ({db_engine})",
version=db_version,
purl=purl,
properties={
"database_engine": db_engine,
"database_name": db_name,
},
source_collector=self.get_collector_name(),
)
)
logger.debug(
"Collected database schema",
name=db_name,
engine=db_engine,
version=db_version,
)
return components
Register in .strata/collectors.yaml:
collectors:
- name: database-schemas
path: .strata/collectors/database_schemas_collector.py
class: DatabaseSchemasCollector
type: collector
Example 5: Custom Collector — External APIs
A collector that documents external API dependencies.
File: .strata/collectors/external_apis_collector.py
"""Collect external API dependencies."""
from pathlib import Path
from typing import List
from strata.builders.sbom.base_sbom_collector import BaseSbomCollector
from strata.models.platform_artifact_model import PlatformArtifactModel
from strata.models.sbom_model import SbomComponentModel
class ExternalApisCollector(BaseSbomCollector):
"""Document external APIs used by the application.
This collector scans configuration files and infrastructure code
to identify external API integrations and document them in the SBOM.
Example components:
- Stripe API v1.3
- Slack Webhooks
- Auth0 Management API
"""
def get_collector_name(self) -> str:
return "external_apis"
def collect(
self,
platform: PlatformArtifactModel,
work_path: Path,
deployment_build_path: Path,
) -> List[SbomComponentModel]:
"""Extract external API components."""
self._reset_warnings()
components: List[SbomComponentModel] = []
# Scan config files for API declarations
config_dir = work_path / "config"
apis_file = config_dir / "external-apis.yaml"
if not apis_file.exists():
self._warnings.append(f"External APIs config not found: {apis_file}")
return components
try:
import yaml
with apis_file.open(encoding="utf-8") as fh:
data = yaml.safe_load(fh) or {}
except Exception as exc:
self._warnings.append(f"Failed to parse {apis_file}: {exc}")
return components
apis = data.get("external_apis") or []
seen_purls: set[str] = set()
for api_spec in apis:
if not isinstance(api_spec, dict):
continue
api_name = api_spec.get("name", "")
api_version = api_spec.get("version", "1.0")
api_url = api_spec.get("url", "")
api_status = api_spec.get("status", "active")
if not api_name:
continue
purl = f"pkg:generic/api/{api_name}@{api_version}"
if purl in seen_purls:
continue
seen_purls.add(purl)
components.append(
SbomComponentModel(
component_type="framework",
name=api_name,
version=api_version,
purl=purl,
properties={
"type": "external_api",
"url": api_url,
"status": api_status,
},
source_collector=self.get_collector_name(),
)
)
return components
Config file (.strata/external-apis.yaml):
external_apis:
- name: Stripe
version: v1.3
url: https://api.stripe.com
status: active
- name: Slack
version: 1.0
url: https://hooks.slack.com
status: active
- name: Auth0
version: 2.0
url: https://auth0.com
status: active
Register in .strata/collectors.yaml:
collectors:
- name: external-apis
path: .strata/collectors/external_apis_collector.py
class: ExternalApisCollector
type: collector
Testing Examples
Test a Lockfile Parser
# tests/sbom/test_bundler_parser.py
from pathlib import Path
import pytest
from strata.builders.sbom.lockfile_parsers.bundler_gemfile import (
BundlerGemdockParser,
)
class TestBundlerParser:
def test_parse_valid_gemfile_lock(self, tmp_path):
"""Parse a valid Gemfile.lock."""
parser = BundlerGemdockParser()
gemfile_lock = tmp_path / "Gemfile.lock"
gemfile_lock.write_text("""
GEM
remote: https://rubygems.org/
specs:
rails (7.0.0)
bundler (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
rails (~> 7.0.0)
BUNDLED WITH
2.3.0
""")
deps = parser.parse(gemfile_lock)
assert len(deps) == 2
assert deps[0].name == "rails"
assert deps[0].version == "7.0.0"
assert deps[1].name == "bundler"
def test_ecosystem(self):
"""Parser identifies as gem ecosystem."""
parser = BundlerGemdockParser()
assert parser.ecosystem == "gem"
def test_filename_patterns(self):
"""Parser matches Gemfile.lock."""
parser = BundlerGemdockParser()
assert "Gemfile.lock" in parser.filename_patterns()
Test a Collector
# tests/sbom/test_database_collector.py
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from strata.builders.sbom.database_schemas_collector import (
DatabaseSchemasCollector,
)
from strata.models.platform_artifact_model import PlatformArtifactModel
class TestDatabaseCollector:
def test_collect_databases(self, tmp_path):
"""Collector extracts database components."""
collector = DatabaseSchemasCollector()
# Mock database objects
db1 = MagicMock()
db1.name = "users_db"
db1.engine = "postgres"
db1.version = "14.5"
db2 = MagicMock()
db2.name = "analytics_db"
db2.engine = "mysql"
db2.version = "8.0.32"
# Mock module with databases
module = MagicMock()
module.databases = [db1, db2]
# Mock platform artifact
platform = MagicMock(spec=PlatformArtifactModel)
platform.spec = MagicMock()
platform.spec.modules = [module]
components = collector.collect(platform, tmp_path, tmp_path / "build")
assert len(components) == 2
assert components[0].name == "users_db (postgres)"
assert components[0].version == "14.5"
assert components[1].name == "analytics_db (mysql)"
def test_collect_deduplicates(self, tmp_path):
"""Collector deduplicates databases."""
collector = DatabaseSchemasCollector()
db = MagicMock()
db.name = "shared_db"
db.engine = "postgres"
db.version = "14.5"
# Same database in two modules
module1 = MagicMock()
module1.databases = [db]
module2 = MagicMock()
module2.databases = [db]
platform = MagicMock(spec=PlatformArtifactModel)
platform.spec = MagicMock()
platform.spec.modules = [module1, module2]
components = collector.collect(platform, tmp_path, tmp_path / "build")
# Should have only one component due to deduplication
assert len(components) == 1
Integration Testing
Test end-to-end SBOM generation with custom plugins:
# Set up test workspace
mkdir -p test-workspace/.strata/lockfile_parsers
mkdir -p test-workspace/.strata/collectors
mkdir -p test-workspace/config
# Copy your plugins
cp bundler_gemfile.py test-workspace/.strata/lockfile_parsers/
cp database_schemas_collector.py test-workspace/.strata/collectors/
# Create collectors.yaml
cat > test-workspace/.strata/collectors.yaml <<EOF
collectors:
- name: database-schemas
path: .strata/collectors/database_schemas_collector.py
class: DatabaseSchemasCollector
type: collector
- name: bundler
path: .strata/lockfile_parsers/bundler_gemfile.py
type: lockfile_parser
EOF
# Create a test deployment file
cat > test-workspace/deploy.yaml <<EOF
apiVersion: strata.huybrechtsxyz/v1
kind: deployment
meta:
name: test-deployment
spec:
modules:
- name: api
services:
- name: web
image: nginx:1.25.0
EOF
# Generate SBOM
cd test-workspace
strata build sbom -f deploy.yaml --output json
# Check the results
cat build/test-deployment-1.0.0/sbom.json | jq '.components | length'
See Also
Extending strata SBOM — Main guide
SBOM Plugin API — API reference
Testing Patterns — More test examples