sbomify-action 0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sbomify_action/__init__.py +81 -0
- sbomify_action/_augmentation/__init__.py +76 -0
- sbomify_action/_augmentation/metadata.py +227 -0
- sbomify_action/_augmentation/protocol.py +80 -0
- sbomify_action/_augmentation/providers/__init__.py +18 -0
- sbomify_action/_augmentation/providers/bitbucket.py +105 -0
- sbomify_action/_augmentation/providers/github.py +97 -0
- sbomify_action/_augmentation/providers/gitlab.py +96 -0
- sbomify_action/_augmentation/providers/json_config.py +157 -0
- sbomify_action/_augmentation/providers/sbomify_api.py +128 -0
- sbomify_action/_augmentation/registry.py +126 -0
- sbomify_action/_augmentation/utils.py +62 -0
- sbomify_action/_enrichment/__init__.py +16 -0
- sbomify_action/_enrichment/enricher.py +274 -0
- sbomify_action/_enrichment/license_db_generator.py +1630 -0
- sbomify_action/_enrichment/license_normalizer.py +534 -0
- sbomify_action/_enrichment/license_utils.py +311 -0
- sbomify_action/_enrichment/lifecycle_data.py +983 -0
- sbomify_action/_enrichment/metadata.py +132 -0
- sbomify_action/_enrichment/protocol.py +89 -0
- sbomify_action/_enrichment/registry.py +128 -0
- sbomify_action/_enrichment/sanitization.py +350 -0
- sbomify_action/_enrichment/sources/__init__.py +27 -0
- sbomify_action/_enrichment/sources/clearlydefined.py +200 -0
- sbomify_action/_enrichment/sources/cratesio.py +187 -0
- sbomify_action/_enrichment/sources/debian.py +257 -0
- sbomify_action/_enrichment/sources/depsdev.py +202 -0
- sbomify_action/_enrichment/sources/ecosystems.py +216 -0
- sbomify_action/_enrichment/sources/license_db.py +703 -0
- sbomify_action/_enrichment/sources/lifecycle.py +214 -0
- sbomify_action/_enrichment/sources/pubdev.py +195 -0
- sbomify_action/_enrichment/sources/purl.py +175 -0
- sbomify_action/_enrichment/sources/pypi.py +197 -0
- sbomify_action/_enrichment/sources/repology.py +245 -0
- sbomify_action/_enrichment/utils.py +98 -0
- sbomify_action/_generation/__init__.py +68 -0
- sbomify_action/_generation/generator.py +187 -0
- sbomify_action/_generation/generators/__init__.py +29 -0
- sbomify_action/_generation/generators/cdxgen.py +339 -0
- sbomify_action/_generation/generators/cyclonedx_cargo.py +161 -0
- sbomify_action/_generation/generators/cyclonedx_py.py +225 -0
- sbomify_action/_generation/generators/syft.py +320 -0
- sbomify_action/_generation/generators/trivy.py +326 -0
- sbomify_action/_generation/protocol.py +225 -0
- sbomify_action/_generation/registry.py +344 -0
- sbomify_action/_generation/result.py +86 -0
- sbomify_action/_generation/utils.py +502 -0
- sbomify_action/_processors/__init__.py +44 -0
- sbomify_action/_processors/orchestrator.py +152 -0
- sbomify_action/_processors/processors/__init__.py +5 -0
- sbomify_action/_processors/processors/releases.py +197 -0
- sbomify_action/_processors/protocol.py +100 -0
- sbomify_action/_processors/registry.py +152 -0
- sbomify_action/_processors/releases_api.py +314 -0
- sbomify_action/_processors/result.py +141 -0
- sbomify_action/_upload/__init__.py +62 -0
- sbomify_action/_upload/destinations/__init__.py +10 -0
- sbomify_action/_upload/destinations/dependency_track.py +252 -0
- sbomify_action/_upload/destinations/sbomify.py +219 -0
- sbomify_action/_upload/orchestrator.py +159 -0
- sbomify_action/_upload/protocol.py +145 -0
- sbomify_action/_upload/registry.py +152 -0
- sbomify_action/_upload/result.py +84 -0
- sbomify_action/additional_packages.py +433 -0
- sbomify_action/augmentation.py +1708 -0
- sbomify_action/cli/__init__.py +25 -0
- sbomify_action/cli/main.py +1923 -0
- sbomify_action/console.py +886 -0
- sbomify_action/enrichment.py +1253 -0
- sbomify_action/exceptions.py +50 -0
- sbomify_action/generation.py +193 -0
- sbomify_action/http_client.py +46 -0
- sbomify_action/logging_config.py +93 -0
- sbomify_action/schemas/cyclonedx/cdx-1.3.schema.json +1058 -0
- sbomify_action/schemas/cyclonedx/cdx-1.4.schema.json +1697 -0
- sbomify_action/schemas/cyclonedx/cdx-1.5.schema.json +3799 -0
- sbomify_action/schemas/cyclonedx/cdx-1.6.schema.json +5699 -0
- sbomify_action/schemas/cyclonedx/cdx-1.7.schema.json +6700 -0
- sbomify_action/schemas/spdx/spdx-2.2.schema.json +721 -0
- sbomify_action/schemas/spdx/spdx-2.3.schema.json +746 -0
- sbomify_action/serialization.py +1031 -0
- sbomify_action/tool_checks.py +338 -0
- sbomify_action/upload.py +180 -0
- sbomify_action/validation.py +352 -0
- sbomify_action-0.12.dist-info/METADATA +854 -0
- sbomify_action-0.12.dist-info/RECORD +89 -0
- sbomify_action-0.12.dist-info/WHEEL +4 -0
- sbomify_action-0.12.dist-info/entry_points.txt +3 -0
- sbomify_action-0.12.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""sbomify-action package for SBOM generation and enrichment."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_display_name(fmt: str) -> str:
|
|
5
|
+
"""Return properly capitalized display name for SBOM format.
|
|
6
|
+
|
|
7
|
+
This is the canonical function for displaying SBOM format names.
|
|
8
|
+
Use this everywhere format names are shown to users.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
fmt: Format string ('cyclonedx' or 'spdx', case-insensitive)
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Display name: 'CycloneDX' or 'SPDX'
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> format_display_name("cyclonedx")
|
|
18
|
+
'CycloneDX'
|
|
19
|
+
>>> format_display_name("SPDX")
|
|
20
|
+
'SPDX'
|
|
21
|
+
"""
|
|
22
|
+
return "CycloneDX" if fmt.lower() == "cyclonedx" else "SPDX"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_version() -> str:
|
|
26
|
+
"""Get package version with fallback mechanisms.
|
|
27
|
+
|
|
28
|
+
Tries the following methods in order until one succeeds:
|
|
29
|
+
- importlib.metadata (preferred for installed packages)
|
|
30
|
+
- tomllib/pyproject.toml (Python 3.11+, for development)
|
|
31
|
+
- toml library/pyproject.toml (older Python, for development)
|
|
32
|
+
- Returns "unknown" if all methods fail
|
|
33
|
+
"""
|
|
34
|
+
# Try importlib.metadata (preferred for installed packages)
|
|
35
|
+
try:
|
|
36
|
+
from importlib.metadata import version
|
|
37
|
+
|
|
38
|
+
return version("sbomify-action")
|
|
39
|
+
except ImportError:
|
|
40
|
+
pass
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try reading from pyproject.toml using tomllib (Python 3.11+)
|
|
45
|
+
try:
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
import tomllib
|
|
49
|
+
|
|
50
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
|
51
|
+
if pyproject_path.exists():
|
|
52
|
+
with open(pyproject_path, "rb") as f:
|
|
53
|
+
pyproject_data = tomllib.load(f)
|
|
54
|
+
return pyproject_data.get("project", {}).get("version", "unknown")
|
|
55
|
+
except ImportError:
|
|
56
|
+
# Python < 3.11 doesn't have tomllib
|
|
57
|
+
pass
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Try toml library as fallback for older Python
|
|
62
|
+
try:
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
import toml
|
|
66
|
+
|
|
67
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
|
68
|
+
if pyproject_path.exists():
|
|
69
|
+
with open(pyproject_path, "r") as f:
|
|
70
|
+
pyproject_data = toml.load(f)
|
|
71
|
+
return pyproject_data.get("project", {}).get("version", "unknown")
|
|
72
|
+
except ImportError:
|
|
73
|
+
pass
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# Final fallback
|
|
78
|
+
return "unknown"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__version__ = _get_version()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Augmentation plugin architecture for SBOM metadata providers.
|
|
2
|
+
|
|
3
|
+
This module provides a plugin-based approach to fetching organizational
|
|
4
|
+
metadata for SBOM augmentation. Multiple providers can supply metadata
|
|
5
|
+
(supplier, authors, licenses, lifecycle_phase, VCS info), which is merged by priority.
|
|
6
|
+
|
|
7
|
+
Providers (in priority order):
|
|
8
|
+
- json-config: Reads from sbomify.json config file (priority 10)
|
|
9
|
+
- github-actions: Auto-detects VCS info from GitHub Actions env (priority 20)
|
|
10
|
+
- gitlab-ci: Auto-detects VCS info from GitLab CI env (priority 20)
|
|
11
|
+
- bitbucket-pipelines: Auto-detects VCS info from Bitbucket Pipelines env (priority 20)
|
|
12
|
+
- sbomify-api: Fetches from sbomify backend API (priority 50)
|
|
13
|
+
|
|
14
|
+
VCS Augmentation:
|
|
15
|
+
CI providers automatically detect repository URL, commit SHA, and branch/ref
|
|
16
|
+
from CI environment variables. This can be:
|
|
17
|
+
- Overridden via sbomify.json config (vcs_url, vcs_commit_sha, vcs_ref)
|
|
18
|
+
- Disabled entirely via DISABLE_VCS_AUGMENTATION=true environment variable
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from sbomify_action._augmentation import create_default_registry
|
|
22
|
+
|
|
23
|
+
registry = create_default_registry()
|
|
24
|
+
metadata = registry.fetch_metadata(
|
|
25
|
+
component_id="xxx",
|
|
26
|
+
api_base_url="https://app.sbomify.com",
|
|
27
|
+
token="your-token",
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .metadata import AugmentationMetadata
|
|
32
|
+
from .protocol import AugmentationProvider
|
|
33
|
+
from .registry import ProviderRegistry
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AugmentationMetadata",
|
|
37
|
+
"AugmentationProvider",
|
|
38
|
+
"ProviderRegistry",
|
|
39
|
+
"create_default_registry",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_default_registry() -> ProviderRegistry:
|
|
44
|
+
"""
|
|
45
|
+
Create a registry with default augmentation providers.
|
|
46
|
+
|
|
47
|
+
Providers are registered in priority order (lower number = higher priority):
|
|
48
|
+
- Priority 10: JsonConfigProvider (local config, can override CI-detected VCS)
|
|
49
|
+
- Priority 20: CI providers (GitHub Actions, GitLab CI, Bitbucket Pipelines)
|
|
50
|
+
- Priority 50: SbomifyApiProvider (backend metadata)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
ProviderRegistry configured with standard providers
|
|
54
|
+
"""
|
|
55
|
+
from .providers import (
|
|
56
|
+
BitbucketPipelinesProvider,
|
|
57
|
+
GitHubActionsProvider,
|
|
58
|
+
GitLabCIProvider,
|
|
59
|
+
JsonConfigProvider,
|
|
60
|
+
SbomifyApiProvider,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
registry = ProviderRegistry()
|
|
64
|
+
|
|
65
|
+
# Priority 10: Local config (can override CI-detected VCS)
|
|
66
|
+
registry.register(JsonConfigProvider())
|
|
67
|
+
|
|
68
|
+
# Priority 20: CI providers (auto-detect VCS from environment)
|
|
69
|
+
registry.register(GitHubActionsProvider())
|
|
70
|
+
registry.register(GitLabCIProvider())
|
|
71
|
+
registry.register(BitbucketPipelinesProvider())
|
|
72
|
+
|
|
73
|
+
# Priority 50: API provider (backend metadata)
|
|
74
|
+
registry.register(SbomifyApiProvider())
|
|
75
|
+
|
|
76
|
+
return registry
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""AugmentationMetadata dataclass for normalized augmentation data."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AugmentationMetadata:
|
|
9
|
+
"""
|
|
10
|
+
Normalized metadata for SBOM augmentation.
|
|
11
|
+
|
|
12
|
+
This dataclass represents the canonical format for augmentation data
|
|
13
|
+
from any provider. It includes fields for NTIA Minimum Elements and
|
|
14
|
+
CISA 2025 requirements.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
supplier: Supplier information (name, urls, contacts)
|
|
18
|
+
manufacturer: Manufacturer information (name, urls, contacts, address)
|
|
19
|
+
authors: List of author information
|
|
20
|
+
licenses: List of license data (strings or dicts)
|
|
21
|
+
lifecycle_phase: CISA 2025 Generation Context (build, post-build, operations, etc.)
|
|
22
|
+
source: Name of the provider that supplied this metadata
|
|
23
|
+
|
|
24
|
+
VCS fields (added by CI providers or sbomify.json config):
|
|
25
|
+
vcs_url: Repository URL (e.g., https://github.com/owner/repo)
|
|
26
|
+
vcs_commit_sha: Full commit SHA
|
|
27
|
+
vcs_ref: Branch or tag name (e.g., main, refs/heads/main, v1.0.0)
|
|
28
|
+
vcs_commit_url: URL to the specific commit
|
|
29
|
+
|
|
30
|
+
Security and support fields (CRA compliance):
|
|
31
|
+
security_contact: URL/email for security vulnerability reporting
|
|
32
|
+
support_period_end: ISO-8601 date when security support ends (YYYY-MM-DD)
|
|
33
|
+
|
|
34
|
+
Lifecycle date fields (from sbomify API):
|
|
35
|
+
release_date: ISO-8601 date when the component was released (YYYY-MM-DD)
|
|
36
|
+
end_of_life: ISO-8601 date when all support ends (YYYY-MM-DD)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
supplier: Optional[Dict[str, Any]] = None
|
|
40
|
+
manufacturer: Optional[Dict[str, Any]] = None
|
|
41
|
+
authors: Optional[List[Dict[str, Any]]] = None
|
|
42
|
+
licenses: Optional[List[Any]] = None
|
|
43
|
+
lifecycle_phase: Optional[str] = None
|
|
44
|
+
source: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
# VCS fields for CI environment enrichment
|
|
47
|
+
vcs_url: Optional[str] = None
|
|
48
|
+
vcs_commit_sha: Optional[str] = None
|
|
49
|
+
vcs_ref: Optional[str] = None
|
|
50
|
+
vcs_commit_url: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
# Security and support fields (CRA compliance)
|
|
53
|
+
security_contact: Optional[str] = None # URL/email for security vulnerability reporting
|
|
54
|
+
support_period_end: Optional[str] = None # ISO-8601 date when support ends (YYYY-MM-DD)
|
|
55
|
+
|
|
56
|
+
# Lifecycle date fields (from sbomify API)
|
|
57
|
+
release_date: Optional[str] = None # ISO-8601 date when released (YYYY-MM-DD)
|
|
58
|
+
end_of_life: Optional[str] = None # ISO-8601 date when all support ends (YYYY-MM-DD)
|
|
59
|
+
|
|
60
|
+
# Additional fields that may be added in the future
|
|
61
|
+
_extra: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def has_data(self) -> bool:
|
|
64
|
+
"""Check if this metadata contains any meaningful data."""
|
|
65
|
+
return any(
|
|
66
|
+
[
|
|
67
|
+
self.supplier,
|
|
68
|
+
self.manufacturer,
|
|
69
|
+
self.authors,
|
|
70
|
+
self.licenses,
|
|
71
|
+
self.lifecycle_phase,
|
|
72
|
+
self.vcs_url,
|
|
73
|
+
self.vcs_commit_sha,
|
|
74
|
+
self.vcs_ref,
|
|
75
|
+
self.vcs_commit_url,
|
|
76
|
+
self.security_contact,
|
|
77
|
+
self.support_period_end,
|
|
78
|
+
self.release_date,
|
|
79
|
+
self.end_of_life,
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def merge(self, other: "AugmentationMetadata") -> "AugmentationMetadata":
|
|
84
|
+
"""
|
|
85
|
+
Merge another metadata instance into this one.
|
|
86
|
+
|
|
87
|
+
The current instance's values take precedence (are not overwritten).
|
|
88
|
+
Only missing fields are filled from the other instance.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
other: Another AugmentationMetadata to merge from
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
New AugmentationMetadata with merged values
|
|
95
|
+
"""
|
|
96
|
+
# Merge sources for attribution
|
|
97
|
+
sources = []
|
|
98
|
+
if self.source:
|
|
99
|
+
sources.append(self.source)
|
|
100
|
+
if other.source and other.source not in sources:
|
|
101
|
+
sources.append(other.source)
|
|
102
|
+
merged_source = ", ".join(sources) if sources else None
|
|
103
|
+
|
|
104
|
+
return AugmentationMetadata(
|
|
105
|
+
supplier=self.supplier if self.supplier else other.supplier,
|
|
106
|
+
manufacturer=self.manufacturer if self.manufacturer else other.manufacturer,
|
|
107
|
+
authors=self.authors if self.authors else other.authors,
|
|
108
|
+
licenses=self.licenses if self.licenses else other.licenses,
|
|
109
|
+
lifecycle_phase=self.lifecycle_phase if self.lifecycle_phase else other.lifecycle_phase,
|
|
110
|
+
source=merged_source,
|
|
111
|
+
# VCS fields
|
|
112
|
+
vcs_url=self.vcs_url if self.vcs_url else other.vcs_url,
|
|
113
|
+
vcs_commit_sha=self.vcs_commit_sha if self.vcs_commit_sha else other.vcs_commit_sha,
|
|
114
|
+
vcs_ref=self.vcs_ref if self.vcs_ref else other.vcs_ref,
|
|
115
|
+
vcs_commit_url=self.vcs_commit_url if self.vcs_commit_url else other.vcs_commit_url,
|
|
116
|
+
# Security and support fields
|
|
117
|
+
security_contact=self.security_contact if self.security_contact else other.security_contact,
|
|
118
|
+
support_period_end=self.support_period_end if self.support_period_end else other.support_period_end,
|
|
119
|
+
# Lifecycle date fields
|
|
120
|
+
release_date=self.release_date if self.release_date else other.release_date,
|
|
121
|
+
end_of_life=self.end_of_life if self.end_of_life else other.end_of_life,
|
|
122
|
+
_extra={**other._extra, **self._extra}, # self takes precedence
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Convert to dictionary format compatible with existing augmentation functions.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary with augmentation data
|
|
131
|
+
"""
|
|
132
|
+
result: Dict[str, Any] = {}
|
|
133
|
+
|
|
134
|
+
if self.supplier:
|
|
135
|
+
result["supplier"] = self.supplier
|
|
136
|
+
if self.manufacturer:
|
|
137
|
+
result["manufacturer"] = self.manufacturer
|
|
138
|
+
if self.authors:
|
|
139
|
+
result["authors"] = self.authors
|
|
140
|
+
if self.licenses:
|
|
141
|
+
result["licenses"] = self.licenses
|
|
142
|
+
if self.lifecycle_phase:
|
|
143
|
+
result["lifecycle_phase"] = self.lifecycle_phase
|
|
144
|
+
|
|
145
|
+
# VCS fields
|
|
146
|
+
if self.vcs_url:
|
|
147
|
+
result["vcs_url"] = self.vcs_url
|
|
148
|
+
if self.vcs_commit_sha:
|
|
149
|
+
result["vcs_commit_sha"] = self.vcs_commit_sha
|
|
150
|
+
if self.vcs_ref:
|
|
151
|
+
result["vcs_ref"] = self.vcs_ref
|
|
152
|
+
if self.vcs_commit_url:
|
|
153
|
+
result["vcs_commit_url"] = self.vcs_commit_url
|
|
154
|
+
|
|
155
|
+
# Security and support fields
|
|
156
|
+
if self.security_contact:
|
|
157
|
+
result["security_contact"] = self.security_contact
|
|
158
|
+
if self.support_period_end:
|
|
159
|
+
result["support_period_end"] = self.support_period_end
|
|
160
|
+
|
|
161
|
+
# Lifecycle date fields
|
|
162
|
+
if self.release_date:
|
|
163
|
+
result["release_date"] = self.release_date
|
|
164
|
+
if self.end_of_life:
|
|
165
|
+
result["end_of_life"] = self.end_of_life
|
|
166
|
+
|
|
167
|
+
# Include any extra fields
|
|
168
|
+
result.update(self._extra)
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, data: Dict[str, Any], source: Optional[str] = None) -> "AugmentationMetadata":
|
|
174
|
+
"""
|
|
175
|
+
Create AugmentationMetadata from a dictionary.
|
|
176
|
+
|
|
177
|
+
Handles both config file field names and sbomify API field names:
|
|
178
|
+
- API uses 'end_of_support' -> maps to 'support_period_end'
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
data: Dictionary with augmentation data
|
|
182
|
+
source: Name of the source that provided this data
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
New AugmentationMetadata instance
|
|
186
|
+
"""
|
|
187
|
+
known_keys = {
|
|
188
|
+
"supplier",
|
|
189
|
+
"manufacturer",
|
|
190
|
+
"authors",
|
|
191
|
+
"licenses",
|
|
192
|
+
"lifecycle_phase",
|
|
193
|
+
"vcs_url",
|
|
194
|
+
"vcs_commit_sha",
|
|
195
|
+
"vcs_ref",
|
|
196
|
+
"vcs_commit_url",
|
|
197
|
+
"security_contact",
|
|
198
|
+
"support_period_end",
|
|
199
|
+
"release_date",
|
|
200
|
+
"end_of_life",
|
|
201
|
+
# API field names (mapped to internal names)
|
|
202
|
+
"end_of_support", # -> support_period_end
|
|
203
|
+
}
|
|
204
|
+
extra = {k: v for k, v in data.items() if k not in known_keys}
|
|
205
|
+
|
|
206
|
+
# Map API field names to internal names
|
|
207
|
+
# API uses 'end_of_support', we use 'support_period_end'
|
|
208
|
+
# Prefer explicit support_period_end, fall back to API's end_of_support
|
|
209
|
+
support_period_end = data.get("support_period_end") or data.get("end_of_support")
|
|
210
|
+
|
|
211
|
+
return cls(
|
|
212
|
+
supplier=data.get("supplier"),
|
|
213
|
+
manufacturer=data.get("manufacturer"),
|
|
214
|
+
authors=data.get("authors"),
|
|
215
|
+
licenses=data.get("licenses"),
|
|
216
|
+
lifecycle_phase=data.get("lifecycle_phase"),
|
|
217
|
+
source=source,
|
|
218
|
+
vcs_url=data.get("vcs_url"),
|
|
219
|
+
vcs_commit_sha=data.get("vcs_commit_sha"),
|
|
220
|
+
vcs_ref=data.get("vcs_ref"),
|
|
221
|
+
vcs_commit_url=data.get("vcs_commit_url"),
|
|
222
|
+
security_contact=data.get("security_contact"),
|
|
223
|
+
support_period_end=support_period_end,
|
|
224
|
+
release_date=data.get("release_date"),
|
|
225
|
+
end_of_life=data.get("end_of_life"),
|
|
226
|
+
_extra=extra,
|
|
227
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""AugmentationProvider protocol for SBOM augmentation plugins."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Protocol
|
|
4
|
+
|
|
5
|
+
from .metadata import AugmentationMetadata
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AugmentationProvider(Protocol):
|
|
9
|
+
"""
|
|
10
|
+
Protocol defining the interface for augmentation provider plugins.
|
|
11
|
+
|
|
12
|
+
Each provider implements this protocol to supply organizational metadata
|
|
13
|
+
for SBOM augmentation (supplier, authors, licenses, lifecycle_phase).
|
|
14
|
+
Providers have priorities - lower numbers indicate higher priority (tried first).
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
class JsonConfigProvider:
|
|
18
|
+
name = "json-config"
|
|
19
|
+
priority = 10 # High priority - local config takes precedence
|
|
20
|
+
|
|
21
|
+
def fetch(self, component_id: str | None = None, **kwargs) -> Optional[AugmentationMetadata]:
|
|
22
|
+
# Read from sbomify.json and return metadata
|
|
23
|
+
...
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Human-readable name of this provider.
|
|
30
|
+
|
|
31
|
+
Used for logging and tracking which provider supplied metadata.
|
|
32
|
+
Examples: "json-config", "sbomify-api", "env-vars"
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def priority(self) -> int:
|
|
38
|
+
"""
|
|
39
|
+
Priority of this provider (lower = higher priority).
|
|
40
|
+
|
|
41
|
+
When multiple providers are available, they are tried in priority order.
|
|
42
|
+
Local/static sources should have low priorities (e.g., 10), API sources
|
|
43
|
+
should have higher priorities (e.g., 50).
|
|
44
|
+
|
|
45
|
+
Recommended priority ranges:
|
|
46
|
+
- 1-20: Local config files (JSON, YAML)
|
|
47
|
+
- 21-40: Environment variables
|
|
48
|
+
- 41-60: API sources (sbomify API)
|
|
49
|
+
- 61-100: Fallback sources
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def fetch(
|
|
54
|
+
self,
|
|
55
|
+
component_id: Optional[str] = None,
|
|
56
|
+
api_base_url: Optional[str] = None,
|
|
57
|
+
token: Optional[str] = None,
|
|
58
|
+
config_path: Optional[str] = None,
|
|
59
|
+
**kwargs,
|
|
60
|
+
) -> Optional[AugmentationMetadata]:
|
|
61
|
+
"""
|
|
62
|
+
Fetch augmentation metadata from this provider.
|
|
63
|
+
|
|
64
|
+
Implementations should:
|
|
65
|
+
1. Retrieve metadata from their source (file, API, env vars, etc.)
|
|
66
|
+
2. Return normalized AugmentationMetadata
|
|
67
|
+
3. Handle errors gracefully (return None on failure)
|
|
68
|
+
4. Set the 'source' field on the returned metadata
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
component_id: Component ID (required for API providers)
|
|
72
|
+
api_base_url: API base URL (for API providers)
|
|
73
|
+
token: Authentication token (for API providers)
|
|
74
|
+
config_path: Path to config file (for file-based providers)
|
|
75
|
+
**kwargs: Additional provider-specific arguments
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
AugmentationMetadata if successful, None if fetch fails or no data
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Augmentation providers for fetching organizational metadata."""
|
|
2
|
+
|
|
3
|
+
# Re-export utility from parent module for backwards compatibility
|
|
4
|
+
from ..utils import is_vcs_augmentation_disabled
|
|
5
|
+
from .bitbucket import BitbucketPipelinesProvider
|
|
6
|
+
from .github import GitHubActionsProvider
|
|
7
|
+
from .gitlab import GitLabCIProvider
|
|
8
|
+
from .json_config import JsonConfigProvider
|
|
9
|
+
from .sbomify_api import SbomifyApiProvider
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BitbucketPipelinesProvider",
|
|
13
|
+
"GitHubActionsProvider",
|
|
14
|
+
"GitLabCIProvider",
|
|
15
|
+
"JsonConfigProvider",
|
|
16
|
+
"SbomifyApiProvider",
|
|
17
|
+
"is_vcs_augmentation_disabled",
|
|
18
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Bitbucket Pipelines provider for VCS augmentation metadata.
|
|
2
|
+
|
|
3
|
+
This provider detects when running in Bitbucket Pipelines and extracts
|
|
4
|
+
VCS information from environment variables. Supports both Bitbucket Cloud
|
|
5
|
+
and attempts best-effort support for Bitbucket Data Center.
|
|
6
|
+
|
|
7
|
+
Environment variables used (Cloud):
|
|
8
|
+
- BITBUCKET_PIPELINE_UUID: Detection (present when in Bitbucket Pipelines)
|
|
9
|
+
- BITBUCKET_GIT_HTTP_ORIGIN: Repository URL
|
|
10
|
+
- BITBUCKET_COMMIT: Full commit SHA
|
|
11
|
+
- BITBUCKET_BRANCH: Branch name (not set for tags)
|
|
12
|
+
- BITBUCKET_TAG: Tag name (not set for branches)
|
|
13
|
+
- BITBUCKET_WORKSPACE: Workspace name
|
|
14
|
+
- BITBUCKET_REPO_SLUG: Repository slug
|
|
15
|
+
|
|
16
|
+
For Bitbucket Data Center, environment variables may differ. Use sbomify.json
|
|
17
|
+
to configure VCS URL manually if auto-detection fails.
|
|
18
|
+
|
|
19
|
+
Set DISABLE_VCS_AUGMENTATION=true to disable VCS enrichment.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from sbomify_action.logging_config import logger
|
|
26
|
+
|
|
27
|
+
from ..metadata import AugmentationMetadata
|
|
28
|
+
from ..utils import is_vcs_augmentation_disabled, truncate_sha
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BitbucketPipelinesProvider:
|
|
32
|
+
"""
|
|
33
|
+
Provider that extracts VCS metadata from Bitbucket Pipelines environment.
|
|
34
|
+
|
|
35
|
+
This provider has priority 20, which is lower than sbomify.json (10),
|
|
36
|
+
allowing local config to override auto-detected values.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str = "bitbucket-pipelines"
|
|
40
|
+
priority: int = 20
|
|
41
|
+
|
|
42
|
+
def fetch(
|
|
43
|
+
self,
|
|
44
|
+
component_id: Optional[str] = None,
|
|
45
|
+
api_base_url: Optional[str] = None,
|
|
46
|
+
token: Optional[str] = None,
|
|
47
|
+
config_path: Optional[str] = None,
|
|
48
|
+
**kwargs,
|
|
49
|
+
) -> Optional[AugmentationMetadata]:
|
|
50
|
+
"""
|
|
51
|
+
Extract VCS metadata from Bitbucket Pipelines environment variables.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
component_id: Ignored (not needed for CI provider)
|
|
55
|
+
api_base_url: Ignored (not needed for CI provider)
|
|
56
|
+
token: Ignored (not needed for CI provider)
|
|
57
|
+
config_path: Ignored (not needed for CI provider)
|
|
58
|
+
**kwargs: Additional arguments (ignored)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
AugmentationMetadata with VCS info if in Bitbucket Pipelines, None otherwise
|
|
62
|
+
"""
|
|
63
|
+
# Check if VCS augmentation is disabled
|
|
64
|
+
if is_vcs_augmentation_disabled():
|
|
65
|
+
logger.debug("VCS augmentation disabled, skipping Bitbucket Pipelines provider")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Check if we're running in Bitbucket Pipelines
|
|
69
|
+
if not os.getenv("BITBUCKET_PIPELINE_UUID"):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
# Extract VCS information
|
|
73
|
+
# BITBUCKET_GIT_HTTP_ORIGIN contains the full repo URL for Cloud
|
|
74
|
+
vcs_url = os.getenv("BITBUCKET_GIT_HTTP_ORIGIN")
|
|
75
|
+
commit_sha = os.getenv("BITBUCKET_COMMIT")
|
|
76
|
+
# Branch or tag
|
|
77
|
+
ref = os.getenv("BITBUCKET_BRANCH") or os.getenv("BITBUCKET_TAG")
|
|
78
|
+
|
|
79
|
+
# Fallback: try to construct URL from workspace and repo slug
|
|
80
|
+
if not vcs_url:
|
|
81
|
+
workspace = os.getenv("BITBUCKET_WORKSPACE")
|
|
82
|
+
repo_slug = os.getenv("BITBUCKET_REPO_SLUG")
|
|
83
|
+
if workspace and repo_slug:
|
|
84
|
+
vcs_url = f"https://bitbucket.org/{workspace}/{repo_slug}"
|
|
85
|
+
logger.debug("Constructed Bitbucket URL from workspace/repo slug")
|
|
86
|
+
|
|
87
|
+
if not vcs_url:
|
|
88
|
+
logger.warning(
|
|
89
|
+
"Bitbucket Pipelines detected but could not determine repository URL. "
|
|
90
|
+
"For Bitbucket Data Center, configure vcs_url in sbomify.json."
|
|
91
|
+
)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Construct commit URL
|
|
95
|
+
vcs_commit_url = f"{vcs_url}/commits/{commit_sha}" if commit_sha else None
|
|
96
|
+
|
|
97
|
+
logger.info(f"Detected Bitbucket Pipelines: {vcs_url} @ {truncate_sha(commit_sha)}")
|
|
98
|
+
|
|
99
|
+
return AugmentationMetadata(
|
|
100
|
+
source=self.name,
|
|
101
|
+
vcs_url=vcs_url,
|
|
102
|
+
vcs_commit_sha=commit_sha,
|
|
103
|
+
vcs_ref=ref,
|
|
104
|
+
vcs_commit_url=vcs_commit_url,
|
|
105
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""GitHub Actions provider for VCS augmentation metadata.
|
|
2
|
+
|
|
3
|
+
This provider detects when running in GitHub Actions and extracts
|
|
4
|
+
VCS information from environment variables. Supports both GitHub.com
|
|
5
|
+
and GitHub Enterprise Server via GITHUB_SERVER_URL.
|
|
6
|
+
|
|
7
|
+
Environment variables used:
|
|
8
|
+
- GITHUB_ACTIONS: Detection ("true" when in GitHub Actions)
|
|
9
|
+
- GITHUB_SERVER_URL: Server URL (e.g., https://github.com or https://github.mycompany.com)
|
|
10
|
+
- GITHUB_REPOSITORY: Repository in owner/repo format
|
|
11
|
+
- GITHUB_SHA: Full commit SHA
|
|
12
|
+
- GITHUB_REF: Git ref (e.g., refs/heads/main, refs/tags/v1.0.0)
|
|
13
|
+
- GITHUB_REF_NAME: Short ref name (e.g., main, v1.0.0)
|
|
14
|
+
|
|
15
|
+
Set DISABLE_VCS_AUGMENTATION=true to disable VCS enrichment.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from sbomify_action.logging_config import logger
|
|
22
|
+
|
|
23
|
+
from ..metadata import AugmentationMetadata
|
|
24
|
+
from ..utils import is_vcs_augmentation_disabled, truncate_sha
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitHubActionsProvider:
|
|
28
|
+
"""
|
|
29
|
+
Provider that extracts VCS metadata from GitHub Actions environment.
|
|
30
|
+
|
|
31
|
+
This provider has priority 20, which is lower than sbomify.json (10),
|
|
32
|
+
allowing local config to override auto-detected values.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name: str = "github-actions"
|
|
36
|
+
priority: int = 20
|
|
37
|
+
|
|
38
|
+
def fetch(
|
|
39
|
+
self,
|
|
40
|
+
component_id: Optional[str] = None,
|
|
41
|
+
api_base_url: Optional[str] = None,
|
|
42
|
+
token: Optional[str] = None,
|
|
43
|
+
config_path: Optional[str] = None,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> Optional[AugmentationMetadata]:
|
|
46
|
+
"""
|
|
47
|
+
Extract VCS metadata from GitHub Actions environment variables.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
component_id: Ignored (not needed for CI provider)
|
|
51
|
+
api_base_url: Ignored (not needed for CI provider)
|
|
52
|
+
token: Ignored (not needed for CI provider)
|
|
53
|
+
config_path: Ignored (not needed for CI provider)
|
|
54
|
+
**kwargs: Additional arguments (ignored)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
AugmentationMetadata with VCS info if in GitHub Actions, None otherwise
|
|
58
|
+
"""
|
|
59
|
+
# Check if VCS augmentation is disabled
|
|
60
|
+
if is_vcs_augmentation_disabled():
|
|
61
|
+
logger.debug("VCS augmentation disabled, skipping GitHub Actions provider")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Check if we're running in GitHub Actions
|
|
65
|
+
if os.getenv("GITHUB_ACTIONS") != "true":
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Extract VCS information
|
|
69
|
+
server_url = os.getenv("GITHUB_SERVER_URL", "https://github.com")
|
|
70
|
+
repository = os.getenv("GITHUB_REPOSITORY")
|
|
71
|
+
commit_sha = os.getenv("GITHUB_SHA")
|
|
72
|
+
ref = os.getenv("GITHUB_REF_NAME") or os.getenv("GITHUB_REF")
|
|
73
|
+
|
|
74
|
+
if not repository:
|
|
75
|
+
logger.warning("GitHub Actions detected but GITHUB_REPOSITORY not set")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Construct URLs
|
|
79
|
+
vcs_url = f"{server_url}/{repository}"
|
|
80
|
+
vcs_commit_url = f"{server_url}/{repository}/commit/{commit_sha}" if commit_sha else None
|
|
81
|
+
|
|
82
|
+
# Clean up ref (remove refs/heads/ or refs/tags/ prefix if present)
|
|
83
|
+
if ref:
|
|
84
|
+
if ref.startswith("refs/heads/"):
|
|
85
|
+
ref = ref[len("refs/heads/") :]
|
|
86
|
+
elif ref.startswith("refs/tags/"):
|
|
87
|
+
ref = ref[len("refs/tags/") :]
|
|
88
|
+
|
|
89
|
+
logger.info(f"Detected GitHub Actions: {repository} @ {truncate_sha(commit_sha)}")
|
|
90
|
+
|
|
91
|
+
return AugmentationMetadata(
|
|
92
|
+
source=self.name,
|
|
93
|
+
vcs_url=vcs_url,
|
|
94
|
+
vcs_commit_sha=commit_sha,
|
|
95
|
+
vcs_ref=ref,
|
|
96
|
+
vcs_commit_url=vcs_commit_url,
|
|
97
|
+
)
|