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
22 changes: 11 additions & 11 deletions .github/workflows/sbomify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,20 +302,20 @@ jobs:
# Arbitrary OS packages (curl, busybox, etc.) do NOT get distro lifecycle
# because the PURL doesn't reliably indicate distro version.

# Check OS component has CLE properties
OS_CLE=$(jq '.components[] | select(.type == "operating-system") | .properties[]? | select(.name | startswith("cle:"))' alpine-enriched.cdx.json)
# Check OS component has CLE properties (using cdx:lifecycle taxonomy)
OS_CLE=$(jq '.components[] | select(.type == "operating-system") | .properties[]? | select(.name | startswith("cdx:lifecycle"))' alpine-enriched.cdx.json)

if [ -n "$OS_CLE" ]; then
echo "✅ OS component has CLE lifecycle properties"
echo "OS component CLE:"
jq '.components[] | select(.type == "operating-system") | {name, version, cle: [.properties[]? | select(.name | startswith("cle:"))]}' alpine-enriched.cdx.json
jq '.components[] | select(.type == "operating-system") | {name, version, cle: [.properties[]? | select(.name | startswith("cdx:lifecycle"))]}' alpine-enriched.cdx.json
else
echo "❌ Expected CLE properties on OS component"
exit 1
fi

# Verify specific CLE values for Alpine 3.20
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:eol") | .value' alpine-enriched.cdx.json)
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:endOfLife") | .value' alpine-enriched.cdx.json)
if [ "$CLE_EOL" = "2026-04-01" ]; then
echo "✅ Alpine 3.20 EOL date is correct: $CLE_EOL"
else
Expand Down Expand Up @@ -429,9 +429,9 @@ jobs:
jq '.components[] | select(.type == "operating-system") | {name, version, publisher}' debian-enriched.cdx.json

# Check for CLE properties
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:eol") | .value' debian-enriched.cdx.json)
CLE_EOS=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:eos") | .value' debian-enriched.cdx.json)
CLE_RELEASE=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:releaseDate") | .value' debian-enriched.cdx.json)
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:endOfLife") | .value' debian-enriched.cdx.json)
CLE_EOS=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:endOfSupport") | .value' debian-enriched.cdx.json)
CLE_RELEASE=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:generalAvailability") | .value' debian-enriched.cdx.json)

echo "CLE Release Date: $CLE_RELEASE"
echo "CLE End of Support: $CLE_EOS"
Expand All @@ -453,7 +453,7 @@ jobs:
echo "=== Verifying Debian 12 CLE Values ==="

# Debian 12 EOL should be 2028-06-30 (LTS end)
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:eol") | .value' debian-enriched.cdx.json)
CLE_EOL=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:endOfLife") | .value' debian-enriched.cdx.json)

if [ "$CLE_EOL" = "2028-06-30" ]; then
echo "✅ Debian 12 EOL date is correct: $CLE_EOL"
Expand All @@ -463,7 +463,7 @@ jobs:
fi

# Debian 12 EOS should be 2026-06-10 (regular support end)
CLE_EOS=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cle:eos") | .value' debian-enriched.cdx.json)
CLE_EOS=$(jq -r '.components[] | select(.type == "operating-system") | .properties[]? | select(.name == "cdx:lifecycle:milestone:endOfSupport") | .value' debian-enriched.cdx.json)

if [ "$CLE_EOS" = "2026-06-10" ]; then
echo "✅ Debian 12 EOS date is correct: $CLE_EOS"
Expand All @@ -488,7 +488,7 @@ jobs:
# Show all CLE-enriched components summary
echo ""
echo "=== All CLE-enriched components ==="
jq -r '.components[] | select(.properties[]?.name | startswith("cle:")) | "\(.type // "library"): \(.name) v\(.version)"' debian-enriched.cdx.json | sort -u
jq -r '.components[] | select(.properties[]?.name | startswith("cdx:lifecycle")) | "\(.type // "library"): \(.name) v\(.version)"' debian-enriched.cdx.json | sort -u

- name: Upload enriched SBOM artifact
uses: actions/upload-artifact@v4
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ Operating system components (CycloneDX `type: operating-system`) are enriched wi
- **OS components**: Detected by CycloneDX `type: operating-system`, matched by name/version
- **Runtimes/frameworks**: Matched by name pattern across all package managers
- Version cycle extracted from full version (e.g., `3.12.7` → `3.12`, `12.12` → `12`)
- CLE properties added: `cle:releaseDate`, `cle:eos`, `cle:eol`
- Lifecycle properties added: `cdx:lifecycle:milestone:generalAvailability`, `cdx:lifecycle:milestone:endOfSupport`, `cdx:lifecycle:milestone:endOfLife`

> **Note**: Arbitrary OS packages (curl, nginx, openssl, etc.) do not receive lifecycle data. Only the operating system itself and explicitly tracked runtimes/frameworks get CLE data.

Expand All @@ -663,9 +663,9 @@ Operating system components (CycloneDX `type: operating-system`) are enriched wi
"name": "debian",
"version": "12.12",
"properties": [
{"name": "cle:releaseDate", "value": "2023-06-10"},
{"name": "cle:eos", "value": "2026-06-10"},
{"name": "cle:eol", "value": "2028-06-30"}
{"name": "cdx:lifecycle:milestone:generalAvailability", "value": "2023-06-10"},
{"name": "cdx:lifecycle:milestone:endOfSupport", "value": "2026-06-10"},
{"name": "cdx:lifecycle:milestone:endOfLife", "value": "2028-06-30"}
]
}
```
Expand Down
2 changes: 1 addition & 1 deletion sbomify_action/_enrichment/sources/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class LifecycleSource:

@property
def name(self) -> str:
return "lifecycle"
return "sbomify-lifecycle-db"

@property
def priority(self) -> int:
Expand Down
54 changes: 11 additions & 43 deletions sbomify_action/augmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from cyclonedx.model.bom import Bom, OrganizationalContact, OrganizationalEntity, Tool
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression
from cyclonedx.model.lifecycle import LifecyclePhase, NamedLifecycle, PredefinedLifecycle
from cyclonedx.model.lifecycle import LifecyclePhase, PredefinedLifecycle
from cyclonedx.model.service import Service
from packageurl import PackageURL
from spdx_tools.spdx.model import (
Expand Down Expand Up @@ -852,61 +852,29 @@ def augment_cyclonedx_sbom(

# Add support period end date
# This satisfies CRA support period requirements
# Add support period end date using official CycloneDX property taxonomy
# See: https://cyclonedx.github.io/cyclonedx-property-taxonomy/cdx/lifecycle.html
if "support_period_end" in augmentation_data and augmentation_data["support_period_end"]:
end_date = augmentation_data["support_period_end"]

# Primary: Use metadata.lifecycles for CDX 1.5+ (native lifecycle support)
if _is_cdx_version_at_least(spec_version, 1, 5):
# Create custom lifecycle to indicate support end
support_lifecycle = NamedLifecycle(
name="support-end",
description=f"Security support ends: {end_date}",
)
bom.metadata.lifecycles.add(support_lifecycle)
logger.info(f"Added support-end lifecycle to CycloneDX: {end_date}")

# For all versions: Add as property with standardized name
# Using cdx: namespace which is conventional for CycloneDX extensions
support_prop = Property(name="cdx:support:enddate", value=end_date)
support_prop = Property(name="cdx:lifecycle:milestone:endOfSupport", value=end_date)
bom.metadata.properties.add(support_prop)
logger.info(f"Added cdx:lifecycle:milestone:endOfSupport property: {end_date}")
audit_trail.record_augmentation("support_period_end", end_date, source="config")

# Add release date
# Records when the component was released
# Add release date using official CycloneDX property taxonomy
if "release_date" in augmentation_data and augmentation_data["release_date"]:
release_date = augmentation_data["release_date"]

# Primary: Use metadata.lifecycles for CDX 1.5+ (native lifecycle support)
if _is_cdx_version_at_least(spec_version, 1, 5):
release_lifecycle = NamedLifecycle(
name="release",
description=f"Released: {release_date}",
)
bom.metadata.lifecycles.add(release_lifecycle)
logger.info(f"Added release lifecycle to CycloneDX: {release_date}")

# For all versions: Add as property with standardized name
release_prop = Property(name="cdx:release:date", value=release_date)
release_prop = Property(name="cdx:lifecycle:milestone:generalAvailability", value=release_date)
bom.metadata.properties.add(release_prop)
logger.info(f"Added cdx:lifecycle:milestone:generalAvailability property: {release_date}")
audit_trail.record_augmentation("release_date", release_date, source="config")

# Add end of life date
# Records when all support ends (beyond security-only support)
# Add end of life date using official CycloneDX property taxonomy
if "end_of_life" in augmentation_data and augmentation_data["end_of_life"]:
eol_date = augmentation_data["end_of_life"]

# Primary: Use metadata.lifecycles for CDX 1.5+ (native lifecycle support)
if _is_cdx_version_at_least(spec_version, 1, 5):
eol_lifecycle = NamedLifecycle(
name="end-of-life",
description=f"End of life: {eol_date}",
)
bom.metadata.lifecycles.add(eol_lifecycle)
logger.info(f"Added end-of-life lifecycle to CycloneDX: {eol_date}")

# For all versions: Add as property with standardized name
eol_prop = Property(name="cdx:eol:date", value=eol_date)
eol_prop = Property(name="cdx:lifecycle:milestone:endOfLife", value=eol_date)
bom.metadata.properties.add(eol_prop)
logger.info(f"Added cdx:lifecycle:milestone:endOfLife property: {eol_date}")
audit_trail.record_augmentation("end_of_life", eol_date, source="config")

return bom
Expand Down
42 changes: 21 additions & 21 deletions sbomify_action/enrichment.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,16 +465,16 @@ def _add_cle_property(name: str, value: str) -> bool:
return True

if metadata.cle_eos:
if _add_cle_property("cle:eos", metadata.cle_eos):
added_fields.append("cle:eos")
if _add_cle_property("cdx:lifecycle:milestone:endOfSupport", metadata.cle_eos):
added_fields.append("cdx:lifecycle:milestone:endOfSupport")

if metadata.cle_eol:
if _add_cle_property("cle:eol", metadata.cle_eol):
added_fields.append("cle:eol")
if _add_cle_property("cdx:lifecycle:milestone:endOfLife", metadata.cle_eol):
added_fields.append("cdx:lifecycle:milestone:endOfLife")

if metadata.cle_release_date:
if _add_cle_property("cle:releaseDate", metadata.cle_release_date):
added_fields.append("cle:releaseDate")
if _add_cle_property("cdx:lifecycle:milestone:generalAvailability", metadata.cle_release_date):
added_fields.append("cdx:lifecycle:milestone:generalAvailability")

# Record to audit trail if any fields were added
if added_fields:
Expand Down Expand Up @@ -613,16 +613,16 @@ def _add_external_ref(category: ExternalPackageRefCategory, ref_type: str, locat
if _add_external_ref(ExternalPackageRefCategory.OTHER, "vcs", metadata.repository_url):
added_fields.append("externalRef (vcs)")

# CLE (Common Lifecycle Enumeration) data - ECMA-428
# For SPDX, we add CLE info to the package comment
# See: https://sbomify.com/compliance/cle/
# CycloneDX lifecycle milestone properties
# For SPDX, we add lifecycle info to the package comment
# See: https://cyclonedx.github.io/cyclonedx-property-taxonomy/cdx/lifecycle.html
cle_parts = []
if metadata.cle_eos:
cle_parts.append(f"cle:eos={metadata.cle_eos}")
cle_parts.append(f"cdx:lifecycle:milestone:endOfSupport={metadata.cle_eos}")
if metadata.cle_eol:
cle_parts.append(f"cle:eol={metadata.cle_eol}")
cle_parts.append(f"cdx:lifecycle:milestone:endOfLife={metadata.cle_eol}")
if metadata.cle_release_date:
cle_parts.append(f"cle:releaseDate={metadata.cle_release_date}")
cle_parts.append(f"cdx:lifecycle:milestone:generalAvailability={metadata.cle_release_date}")

if cle_parts:
cle_comment = f"CLE lifecycle: {', '.join(cle_parts)}"
Expand Down Expand Up @@ -671,16 +671,16 @@ def _add_cle_property(name: str, value: str) -> bool:
return True

if lifecycle.get("release_date"):
if _add_cle_property("cle:releaseDate", lifecycle["release_date"]):
added_fields.append(f"cle:releaseDate ({lifecycle['release_date']})")
if _add_cle_property("cdx:lifecycle:milestone:generalAvailability", lifecycle["release_date"]):
added_fields.append(f"cdx:lifecycle:milestone:generalAvailability ({lifecycle['release_date']})")

if lifecycle.get("end_of_support"):
if _add_cle_property("cle:eos", lifecycle["end_of_support"]):
added_fields.append(f"cle:eos ({lifecycle['end_of_support']})")
if _add_cle_property("cdx:lifecycle:milestone:endOfSupport", lifecycle["end_of_support"]):
added_fields.append(f"cdx:lifecycle:milestone:endOfSupport ({lifecycle['end_of_support']})")

if lifecycle.get("end_of_life"):
if _add_cle_property("cle:eol", lifecycle["end_of_life"]):
added_fields.append(f"cle:eol ({lifecycle['end_of_life']})")
if _add_cle_property("cdx:lifecycle:milestone:endOfLife", lifecycle["end_of_life"]):
added_fields.append(f"cdx:lifecycle:milestone:endOfLife ({lifecycle['end_of_life']})")

return added_fields

Expand Down Expand Up @@ -861,11 +861,11 @@ def _enrich_spdx_os_packages(document: Document) -> Dict[str, int]:
# Build lifecycle comment
lifecycle_parts = []
if lifecycle.get("release_date"):
lifecycle_parts.append(f"cle:releaseDate={lifecycle['release_date']}")
lifecycle_parts.append(f"cdx:lifecycle:milestone:generalAvailability={lifecycle['release_date']}")
if lifecycle.get("end_of_support"):
lifecycle_parts.append(f"cle:eos={lifecycle['end_of_support']}")
lifecycle_parts.append(f"cdx:lifecycle:milestone:endOfSupport={lifecycle['end_of_support']}")
if lifecycle.get("end_of_life"):
lifecycle_parts.append(f"cle:eol={lifecycle['end_of_life']}")
lifecycle_parts.append(f"cdx:lifecycle:milestone:endOfLife={lifecycle['end_of_life']}")

if lifecycle_parts:
lifecycle_comment = COMMENT_DELIMITER.join(lifecycle_parts)
Expand Down
46 changes: 18 additions & 28 deletions tests/cra_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
CycloneDX (1.5+ for full support, 1.3-1.4 partial):
- Security Contact: metadata.component.externalReferences[type=security-contact] (1.5+)
OR metadata.supplier.contacts[] (1.3-1.4 fallback)
- Release Date: metadata.lifecycles[name=release] OR metadata.properties[name=cdx:release:date]
- Support Period End: metadata.lifecycles[name=support-end] OR metadata.properties[name=cdx:support:enddate]
- End of Life: metadata.lifecycles[name=end-of-life] OR metadata.properties[name=cdx:eol:date]
- Release Date: metadata.properties[name=cdx:lifecycle:milestone:generalAvailability]
- Support Period End: metadata.properties[name=cdx:lifecycle:milestone:endOfSupport]
- End of Life: metadata.properties[name=cdx:lifecycle:milestone:endOfLife]

SPDX (2.2 and 2.3):
- Security Contact: packages[].externalRefs[referenceType=security-contact]
Expand Down Expand Up @@ -130,16 +130,12 @@ def check_cyclonedx(
missing.append("Security Contact")

# 2. Release Date (Recommended)
# Check named lifecycle first
release_lifecycle = cls._find_named_lifecycle(lifecycles, "release")
release_date = None
if release_lifecycle:
# Extract date from description or use the lifecycle
release_date = release_lifecycle.get("description", "")

# Check property fallback
# Check property first (official CycloneDX taxonomy), then lifecycle for backward compat
release_date = cls._find_property(properties, "cdx:lifecycle:milestone:generalAvailability")
if not release_date:
release_date = cls._find_property(properties, "cdx:release:date")
release_lifecycle = cls._find_named_lifecycle(lifecycles, "release")
if release_lifecycle:
release_date = release_lifecycle.get("description", "")

if release_date:
present.append("Release Date")
Expand All @@ -148,15 +144,12 @@ def check_cyclonedx(
missing.append("Release Date")

# 3. Support Period End (Required for CRA)
# Check named lifecycle first
support_lifecycle = cls._find_named_lifecycle(lifecycles, "support-end")
support_end = None
if support_lifecycle:
support_end = support_lifecycle.get("description", "")

# Check property fallback
# Check property first (official CycloneDX taxonomy), then lifecycle for backward compat
support_end = cls._find_property(properties, "cdx:lifecycle:milestone:endOfSupport")
if not support_end:
support_end = cls._find_property(properties, "cdx:support:enddate")
support_lifecycle = cls._find_named_lifecycle(lifecycles, "support-end")
if support_lifecycle:
support_end = support_lifecycle.get("description", "")

if support_end:
present.append("Support Period End")
Expand All @@ -165,15 +158,12 @@ def check_cyclonedx(
missing.append("Support Period End")

# 4. End of Life (Recommended)
# Check named lifecycle first
eol_lifecycle = cls._find_named_lifecycle(lifecycles, "end-of-life")
end_of_life = None
if eol_lifecycle:
end_of_life = eol_lifecycle.get("description", "")

# Check property fallback
# Check property first (official CycloneDX taxonomy), then lifecycle for backward compat
end_of_life = cls._find_property(properties, "cdx:lifecycle:milestone:endOfLife")
if not end_of_life:
end_of_life = cls._find_property(properties, "cdx:eol:date")
eol_lifecycle = cls._find_named_lifecycle(lifecycles, "end-of-life")
if eol_lifecycle:
end_of_life = eol_lifecycle.get("description", "")

if end_of_life:
present.append("End of Life")
Expand Down
Loading