Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ That's it! This generates an SBOM from your lockfile and enriches it with metada
- **Generate** SBOMs from lockfiles (Python, Node, Rust, Go, Ruby, Dart, C++)
- **Generate** SBOMs from Docker images
- **Inject** additional packages not in lockfiles (vendored code, runtime deps, system libraries)
- **Augment** with business metadata (supplier, authors, licenses) from sbomify
- **Augment** with business metadata (supplier, authors, licenses, lifecycle phase) from config file or sbomify
- **Enrich** with package metadata from PyPI, pub.dev, npm, Maven, deps.dev, and more
- **Upload** to sbomify for collaboration and vulnerability management
- **Tag** SBOMs with product releases
Expand All @@ -48,6 +48,22 @@ That's it! This generates an SBOM from your lockfile and enriches it with metada
ENRICH: true
```

### Standalone with Augmentation

Add business metadata without a sbomify account using a local config file:

```yaml
- uses: sbomify/github-action@master
env:
LOCK_FILE: requirements.txt
OUTPUT_FILE: sbom.cdx.json
UPLOAD: false
AUGMENT: true # Uses sbomify.json in project root
ENRICH: true
```

See [Augmentation Config File](#augmentation-config-file) for the config format.

### With sbomify

```yaml
Expand Down Expand Up @@ -215,6 +231,7 @@ pkg:deb/debian/openssl@3.0.11
```

**File format:**

- One [PURL](https://github.com/package-url/purl-spec) per line
- Lines starting with `#` are comments
- Empty lines are ignored
Expand Down Expand Up @@ -304,10 +321,49 @@ docker run --rm -v $(pwd):/code \

## Augmentation vs Enrichment

**Augmentation** (`AUGMENT=true`) adds your business metadata from sbomify—supplier info, authors, and licenses you've configured for your component. This requires a sbomify account.
**Augmentation** (`AUGMENT=true`) adds organizational metadata to your SBOM—supplier info, authors, licenses, and lifecycle phase. This addresses [NTIA Minimum Elements](https://sbomify.com/compliance/ntia-minimum-elements/) and [CISA 2025](https://sbomify.com/compliance/cisa-minimum-elements/) requirements.

Augmentation sources (in priority order):

1. **Local config file** (`sbomify.json`) — No account needed. Local values take precedence.
2. **sbomify API** — Fetches metadata configured in your sbomify component. Requires account.

**Enrichment** (`ENRICH=true`) fetches package metadata from public registries. No account needed.

### Augmentation Config File

Create `sbomify.json` in your project root to provide augmentation metadata:

```json
{
"lifecycle_phase": "build",
"supplier": {
"name": "My Company",
"url": ["https://example.com"],
"contacts": [{"name": "Support", "email": "support@example.com"}]
},
"authors": [
{"name": "John Doe", "email": "john@example.com"}
],
"licenses": ["MIT"]
}
```

**Supported fields:**

| Field | Description | SBOM Mapping |
|-------|-------------|--------------|
| `lifecycle_phase` | Generation context (CISA 2025) | CycloneDX 1.5+: `metadata.lifecycles[].phase`; SPDX: `creationInfo.creatorComment` |
| `supplier` | Organization that supplies the component | CycloneDX: `metadata.supplier`; SPDX: `packages[].supplier` |
| `authors` | List of component authors | CycloneDX: `metadata.authors[]`; SPDX: `creationInfo.creators[]` |
| `licenses` | SPDX license identifiers | CycloneDX: `metadata.licenses[]`; SPDX: Document-level licenses |

**Valid `lifecycle_phase` values:** `design`, `pre-build`, `build`, `post-build`, `operations`, `discovery`, `decommission`

**Priority:** Local config values override sbomify API values when both are available.

### Enrichment Data Sources

| Source | Package Types | Data |
|--------|---------------|------|
| PyPI | Python | License, author, homepage |
Expand Down
37 changes: 34 additions & 3 deletions docs/ntia_comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,29 @@ SBOM generators like Trivy and Syft produce minimal component data—typically j
sbomify enriches every component with metadata from authoritative package registries, making your SBOMs
more useful for vulnerability management, license compliance, and supply chain analysis.

### What sbomify Adds
### NTIA Minimum Elements

Most SBOM generators only include basic package identification (name, version, PURL).
sbomify enriches each component with **7 additional fields** from authoritative sources:
sbomify helps you achieve compliance with the [NTIA Minimum Elements for SBOM](https://sbomify.com/compliance/ntia-minimum-elements/),
the foundational US baseline for SBOM data fields. The table below shows how sbomify contributes to each required element:

| NTIA Element | CycloneDX Field | SPDX Field | Provided By |
|--------------|-----------------|------------|-------------|
| **Supplier Name** | `components[].publisher` or `supplier.name` | `packages[].supplier` | Enrichment |
| **Component Name** | `components[].name` | `packages[].name` | Generator |
| **Component Version** | `components[].version` | `packages[].versionInfo` | Generator |
| **Unique Identifiers** | `components[].purl` | `packages[].externalRefs[].purl` | Generator |
| **Dependency Relationship** | `dependencies[]` | `relationships[]` | Generator |
| **SBOM Author** | `metadata.authors[]` | `creationInfo.creators[]` | Augmentation |
| **Timestamp** | `metadata.timestamp` | `creationInfo.created` | Generator |

For the complete field mapping across CycloneDX and SPDX versions, see the
[Schema Crosswalk](https://sbomify.com/compliance/schema-crosswalk/).

### Enrichment: Beyond NTIA Minimums

Beyond the NTIA minimum elements, sbomify enriches each component with **7 additional metadata fields**
from authoritative package registries. These fields improve SBOM utility for vulnerability management,
license compliance, and supply chain analysis:

| Ecosystem | Scanner | Metadata Fields | | |
|-----------|---------|-----------------|--|--|
Expand Down Expand Up @@ -103,4 +122,16 @@ sbomify queries multiple authoritative sources in priority order:
}
```

### CISA 2025 Additional Fields

The [CISA 2025 Minimum Elements](https://sbomify.com/compliance/cisa-minimum-elements/) draft introduces
additional fields beyond NTIA 2021. sbomify supports these where applicable:

| CISA 2025 Field | CycloneDX Field | SPDX Field | Status |
|-----------------|-----------------|------------|--------|
| **Component Hash** | `components[].hashes[]` | `packages[].checksums[]` | From generators |
| **License** | `components[].licenses[]` | `packages[].licenseDeclared` | Enrichment adds |
| **Tool Name/Version** | `metadata.tools` | `creationInfo.creators[]` | Augmentation adds sbomify |
| **Generation Context** | `metadata.lifecycles[].phase` (1.5+) | `creationInfo.creatorComment` | Augmentation adds from backend |

*Generated: 2025-12-17 — Run `uv run scripts/generate_ntia_comparison.py` to update*
47 changes: 47 additions & 0 deletions sbomify_action/_augmentation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Augmentation plugin architecture for SBOM metadata providers.

This module provides a plugin-based approach to fetching organizational
metadata for SBOM augmentation. Multiple providers can supply metadata
(supplier, authors, licenses, lifecycle_phase), which is merged by priority.

Providers:
- json-config: Reads from sbomify.json config file (priority 10)
- sbomify-api: Fetches from sbomify backend API (priority 50)

Usage:
from sbomify_action._augmentation import create_default_registry

registry = create_default_registry()
metadata = registry.fetch_metadata(
component_id="xxx",
api_base_url="https://app.sbomify.com",
token="your-token",
)
"""

from .metadata import AugmentationMetadata
from .protocol import AugmentationProvider
from .registry import ProviderRegistry

__all__ = [
"AugmentationMetadata",
"AugmentationProvider",
"ProviderRegistry",
"create_default_registry",
]


def create_default_registry() -> ProviderRegistry:
"""
Create a registry with default augmentation providers.

Returns:
ProviderRegistry configured with standard providers
"""
from .providers import JsonConfigProvider, SbomifyApiProvider

registry = ProviderRegistry()
registry.register(JsonConfigProvider())
registry.register(SbomifyApiProvider())

return registry
119 changes: 119 additions & 0 deletions sbomify_action/_augmentation/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""AugmentationMetadata dataclass for normalized augmentation data."""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass
class AugmentationMetadata:
"""
Normalized metadata for SBOM augmentation.

This dataclass represents the canonical format for augmentation data
from any provider. It includes fields for NTIA Minimum Elements and
CISA 2025 requirements.

Attributes:
supplier: Supplier information (name, urls, contacts)
authors: List of author information
licenses: List of license data (strings or dicts)
lifecycle_phase: CISA 2025 Generation Context (build, post-build, operations, etc.)
source: Name of the provider that supplied this metadata
"""

supplier: Optional[Dict[str, Any]] = None
authors: Optional[List[Dict[str, Any]]] = None
licenses: Optional[List[Any]] = None
lifecycle_phase: Optional[str] = None
source: Optional[str] = None

# Additional fields that may be added in the future
_extra: Dict[str, Any] = field(default_factory=dict)

def has_data(self) -> bool:
"""Check if this metadata contains any meaningful data."""
return any(
[
self.supplier,
self.authors,
self.licenses,
self.lifecycle_phase,
]
)

def merge(self, other: "AugmentationMetadata") -> "AugmentationMetadata":
"""
Merge another metadata instance into this one.

The current instance's values take precedence (are not overwritten).
Only missing fields are filled from the other instance.

Args:
other: Another AugmentationMetadata to merge from

Returns:
New AugmentationMetadata with merged values
"""
# Merge sources for attribution
sources = []
if self.source:
sources.append(self.source)
if other.source and other.source not in sources:
sources.append(other.source)
merged_source = ", ".join(sources) if sources else None

return AugmentationMetadata(
supplier=self.supplier if self.supplier else other.supplier,
authors=self.authors if self.authors else other.authors,
licenses=self.licenses if self.licenses else other.licenses,
lifecycle_phase=self.lifecycle_phase if self.lifecycle_phase else other.lifecycle_phase,
source=merged_source,
_extra={**other._extra, **self._extra}, # self takes precedence
)

def to_dict(self) -> Dict[str, Any]:
"""
Convert to dictionary format compatible with existing augmentation functions.

Returns:
Dictionary with augmentation data
"""
result: Dict[str, Any] = {}

if self.supplier:
result["supplier"] = self.supplier
if self.authors:
result["authors"] = self.authors
if self.licenses:
result["licenses"] = self.licenses
if self.lifecycle_phase:
result["lifecycle_phase"] = self.lifecycle_phase

# Include any extra fields
result.update(self._extra)

return result

@classmethod
def from_dict(cls, data: Dict[str, Any], source: Optional[str] = None) -> "AugmentationMetadata":
"""
Create AugmentationMetadata from a dictionary.

Args:
data: Dictionary with augmentation data
source: Name of the source that provided this data

Returns:
New AugmentationMetadata instance
"""
known_keys = {"supplier", "authors", "licenses", "lifecycle_phase"}
extra = {k: v for k, v in data.items() if k not in known_keys}

return cls(
supplier=data.get("supplier"),
authors=data.get("authors"),
licenses=data.get("licenses"),
lifecycle_phase=data.get("lifecycle_phase"),
source=source,
_extra=extra,
)
80 changes: 80 additions & 0 deletions sbomify_action/_augmentation/protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""AugmentationProvider protocol for SBOM augmentation plugins."""

from typing import Optional, Protocol

from .metadata import AugmentationMetadata


class AugmentationProvider(Protocol):
"""
Protocol defining the interface for augmentation provider plugins.

Each provider implements this protocol to supply organizational metadata
for SBOM augmentation (supplier, authors, licenses, lifecycle_phase).
Providers have priorities - lower numbers indicate higher priority (tried first).

Example:
class JsonConfigProvider:
name = "json-config"
priority = 10 # High priority - local config takes precedence

def fetch(self, component_id: str | None = None, **kwargs) -> Optional[AugmentationMetadata]:
# Read from sbomify.json and return metadata
...
"""

@property
def name(self) -> str:
"""
Human-readable name of this provider.

Used for logging and tracking which provider supplied metadata.
Examples: "json-config", "sbomify-api", "env-vars"
"""
...

@property
def priority(self) -> int:
"""
Priority of this provider (lower = higher priority).

When multiple providers are available, they are tried in priority order.
Local/static sources should have low priorities (e.g., 10), API sources
should have higher priorities (e.g., 50).

Recommended priority ranges:
- 1-20: Local config files (JSON, YAML)
- 21-40: Environment variables
- 41-60: API sources (sbomify API)
- 61-100: Fallback sources
"""
...

def fetch(
self,
component_id: Optional[str] = None,
api_base_url: Optional[str] = None,
token: Optional[str] = None,
config_path: Optional[str] = None,
**kwargs,
) -> Optional[AugmentationMetadata]:
"""
Fetch augmentation metadata from this provider.

Implementations should:
1. Retrieve metadata from their source (file, API, env vars, etc.)
2. Return normalized AugmentationMetadata
3. Handle errors gracefully (return None on failure)
4. Set the 'source' field on the returned metadata

Args:
component_id: Component ID (required for API providers)
api_base_url: API base URL (for API providers)
token: Authentication token (for API providers)
config_path: Path to config file (for file-based providers)
**kwargs: Additional provider-specific arguments

Returns:
AugmentationMetadata if successful, None if fetch fails or no data
"""
...
9 changes: 9 additions & 0 deletions sbomify_action/_augmentation/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Augmentation providers for fetching organizational metadata."""

from .json_config import JsonConfigProvider
from .sbomify_api import SbomifyApiProvider

__all__ = [
"JsonConfigProvider",
"SbomifyApiProvider",
]
Loading