-
Notifications
You must be signed in to change notification settings - Fork 53
feat(materials): extract main component info from SPDX files #2984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
732b95a
9e0f4dc
1baeeee
c881d27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,11 @@ import ( | |
| schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" | ||
| api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" | ||
| "github.com/chainloop-dev/chainloop/pkg/casclient" | ||
| remotename "github.com/google/go-containerregistry/pkg/name" | ||
| "github.com/rs/zerolog" | ||
| "github.com/spdx/tools-golang/json" | ||
| "github.com/spdx/tools-golang/spdx" | ||
|
|
||
| "github.com/rs/zerolog" | ||
| "github.com/spdx/tools-golang/spdxlib" | ||
| ) | ||
|
|
||
| type SPDXJSONCrafter struct { | ||
|
|
@@ -65,13 +66,87 @@ func (i *SPDXJSONCrafter) Craft(ctx context.Context, filePath string) (*api.Atte | |
| return nil, err | ||
| } | ||
|
|
||
| res := m | ||
| res.M = &api.Attestation_Material_SbomArtifact{ | ||
| SbomArtifact: &api.Attestation_Material_SBOMArtifact{ | ||
| Artifact: m.GetArtifact(), | ||
| }, | ||
| } | ||
|
|
||
| // Extract main component information from SPDX document | ||
| if err := i.extractMainComponent(m, doc); err != nil { | ||
| i.logger.Debug().Err(err).Msg("error extracting main component from spdx sbom, skipping...") | ||
| } | ||
|
|
||
| i.injectAnnotations(m, doc) | ||
|
|
||
| return m, nil | ||
| return res, nil | ||
| } | ||
|
|
||
| // extractMainComponent inspects the SPDX document and extracts the main component if any. | ||
| // It uses the first described package (via DESCRIBES relationship). If multiple described | ||
| // packages exist, only the first is used and a warning is logged. | ||
| // NOTE: SPDX PrimaryPackagePurpose values (APPLICATION, CONTAINER, FRAMEWORK, LIBRARY, etc.) | ||
| // are lowercased for consistency with CycloneDX component types. The two specs have different | ||
| // vocabularies so consumers should handle both sets of values. | ||
| func (i *SPDXJSONCrafter) extractMainComponent(m *api.Attestation_Material, doc *spdx.Document) error { | ||
| describedIDs, err := spdxlib.GetDescribedPackageIDs(doc) | ||
| if err != nil { | ||
| return fmt.Errorf("couldn't get described packages: %w", err) | ||
| } | ||
|
|
||
| if len(describedIDs) == 0 { | ||
| return fmt.Errorf("no described packages found") | ||
| } | ||
|
|
||
| if len(describedIDs) > 1 { | ||
| i.logger.Warn().Int("count", len(describedIDs)).Msg("multiple described packages found, using the first one") | ||
| } | ||
|
|
||
| // Use the first described package | ||
| targetID := describedIDs[0] | ||
|
|
||
| // Find the package by ID | ||
| var describedPkg *spdx.Package | ||
| for _, pkg := range doc.Packages { | ||
| if pkg.PackageSPDXIdentifier == targetID { | ||
| describedPkg = pkg | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if describedPkg == nil { | ||
| return fmt.Errorf("described package %q not found in packages list", targetID) | ||
| } | ||
|
|
||
| name := describedPkg.PackageName | ||
| version := describedPkg.PackageVersion | ||
|
|
||
| // PrimaryPackagePurpose is optional in SPDX 2.3. Best effort: return name | ||
| // and version even if kind is unknown. | ||
| kind := strings.ToLower(describedPkg.PrimaryPackagePurpose) | ||
|
|
||
| // For container packages, standardize the name via go-containerregistry | ||
| // to get the full repository name and strip any tag (matching CycloneDX behavior). | ||
| // If parsing fails (e.g. missing registry credentials), continue with the original name. | ||
| if kind == containerComponentKind { | ||
| ref, err := remotename.ParseReference(name) | ||
| if err != nil { | ||
| i.logger.Debug().Err(err).Str("name", name).Msg("couldn't parse OCI image reference, using original name") | ||
| } else { | ||
| name = ref.Context().String() | ||
| } | ||
| } | ||
|
|
||
| setMainComponent(m, name, kind, version) | ||
|
|
||
| return nil | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This type-assertion + assignment block is identical to the one in func setMainComponent(m *api.Attestation_Material, name, kind, version string) {
m.M.(*api.Attestation_Material_SbomArtifact).SbomArtifact.MainComponent =
&api.Attestation_Material_SBOMArtifact_MainComponent{
Name: name, Kind: kind, Version: version,
}
}Also — there's an existing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great suggestion — extracted a shared |
||
|
|
||
| func (i *SPDXJSONCrafter) injectAnnotations(m *api.Attestation_Material, doc *spdx.Document) { | ||
| m.Annotations = make(map[string]string) | ||
| if m.Annotations == nil { | ||
| m.Annotations = make(map[string]string) | ||
| } | ||
|
|
||
| // Extract all tools from the creators array | ||
| var tools []Tool | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| { | ||
| "spdxVersion": "SPDX-2.3", | ||
| "dataLicense": "CC0-1.0", | ||
| "SPDXID": "SPDXRef-DOCUMENT", | ||
| "name": "ghcr.io/chainloop-dev/chainloop/control-plane", | ||
| "documentNamespace": "https://example.com/test/control-plane-5678", | ||
| "creationInfo": { | ||
| "licenseListVersion": "3.20", | ||
| "creators": [ | ||
| "Organization: Example, Inc", | ||
| "Tool: trivy-0.50.0" | ||
| ], | ||
| "created": "2024-01-15T10:00:00Z" | ||
| }, | ||
| "packages": [ | ||
| { | ||
| "name": "ghcr.io/chainloop-dev/chainloop/control-plane:v0.55.0", | ||
| "SPDXID": "SPDXRef-Package-control-plane", | ||
| "versionInfo": "sha256:abcdef1234567890", | ||
| "downloadLocation": "NOASSERTION", | ||
| "primaryPackagePurpose": "CONTAINER", | ||
| "licenseConcluded": "NOASSERTION", | ||
| "licenseDeclared": "NOASSERTION", | ||
| "copyrightText": "NOASSERTION", | ||
| "supplier": "Organization: Chainloop" | ||
| }, | ||
| { | ||
| "name": "libc", | ||
| "SPDXID": "SPDXRef-Package-libc", | ||
| "versionInfo": "2.31", | ||
| "downloadLocation": "NOASSERTION", | ||
| "licenseConcluded": "GPL-2.0-only", | ||
| "licenseDeclared": "GPL-2.0-only", | ||
| "copyrightText": "NOASSERTION" | ||
| } | ||
| ], | ||
| "relationships": [ | ||
| { | ||
| "spdxElementId": "SPDXRef-DOCUMENT", | ||
| "relatedSpdxElement": "SPDXRef-Package-control-plane", | ||
| "relationshipType": "DESCRIBES" | ||
| }, | ||
| { | ||
| "spdxElementId": "SPDXRef-Package-control-plane", | ||
| "relatedSpdxElement": "SPDXRef-Package-libc", | ||
| "relationshipType": "CONTAINS" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "spdxVersion": "SPDX-2.3", | ||
| "dataLicense": "CC0-1.0", | ||
| "SPDXID": "SPDXRef-DOCUMENT", | ||
| "name": "my-lib", | ||
| "documentNamespace": "https://example.com/test/my-lib-9999", | ||
| "creationInfo": { | ||
| "licenseListVersion": "3.20", | ||
| "creators": [ | ||
| "Organization: Example, Inc", | ||
| "Tool: syft-0.100.0" | ||
| ], | ||
| "created": "2024-01-15T10:00:00Z" | ||
| }, | ||
| "packages": [ | ||
| { | ||
| "name": "my-lib", | ||
| "SPDXID": "SPDXRef-Package-my-lib", | ||
| "versionInfo": "2.0.0", | ||
| "downloadLocation": "https://example.com/my-lib", | ||
| "licenseConcluded": "MIT", | ||
| "licenseDeclared": "MIT", | ||
| "copyrightText": "NOASSERTION", | ||
| "supplier": "Organization: Example, Inc" | ||
| } | ||
| ], | ||
| "relationships": [ | ||
| { | ||
| "spdxElementId": "SPDXRef-DOCUMENT", | ||
| "relatedSpdxElement": "SPDXRef-Package-my-lib", | ||
| "relationshipType": "DESCRIBES" | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it's likely that chainloop doesn't have the credentials for this query in most cases. Instead of returning, I'd just continue and return the existing values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense — chainloop likely won't have registry credentials in most cases. Now logs at debug level and continues with the original name instead of returning an error.