diff --git a/pkg/attestation/crafter/materials/csaf.go b/pkg/attestation/crafter/materials/csaf.go index f7749fce0..a19cff11f 100644 --- a/pkg/attestation/crafter/materials/csaf.go +++ b/pkg/attestation/crafter/materials/csaf.go @@ -129,5 +129,30 @@ func (i *CSAFCrafter) Craft(ctx context.Context, filepath string) (*api.Attestat return nil, fmt.Errorf("invalid CSAF file: %w", ErrInvalidMaterialType) } - return uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) + m, err := uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, documentMap) + + return m, nil +} + +func (i *CSAFCrafter) injectAnnotations(m *api.Attestation_Material, documentMap map[string]any) { + m.Annotations = make(map[string]string) + + // extract vendor info + if tracking, ok := documentMap["tracking"].(map[string]any); ok { + if generator, ok := tracking["generator"].(map[string]any); ok { + if engine, ok := generator["engine"].(map[string]any); ok { + if name, ok := engine["name"].(string); ok { + m.Annotations[AnnotationToolNameKey] = name + } + if version, ok := engine["version"].(string); ok { + m.Annotations[AnnotationToolVersionKey] = version + } + } + } + } } diff --git a/pkg/attestation/crafter/materials/cyclonedxjson.go b/pkg/attestation/crafter/materials/cyclonedxjson.go index 80bd4a4f3..45366fda2 100644 --- a/pkg/attestation/crafter/materials/cyclonedxjson.go +++ b/pkg/attestation/crafter/materials/cyclonedxjson.go @@ -27,7 +27,6 @@ import ( 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" ) @@ -43,19 +42,38 @@ type CyclonedxJSONCrafter struct { *crafterCommon } -// mainComponentStruct internal struct to unmarshall the incoming CycloneDX JSON -type mainComponentStruct struct { - Metadata struct { - Component struct { - Name string `json:"name"` - Type string `json:"type"` - Version string `json:"version"` - Properties []struct { - Name string `json:"name"` - Value string `json:"value"` - } `json:"properties"` - } `json:"component"` - } `json:"metadata"` +// cyclonedxDoc internal struct to unmarshall the incoming CycloneDX JSON +type cyclonedxDoc struct { + SpecVersion string `json:"specVersion"` + Metadata json.RawMessage `json:"metadata"` +} + +type cyclonedxMetadataV14 struct { + Tools []struct { + Name string `json:"name"` + Version string `json:"version"` + } `json:"tools"` + Component cyclonedxComponent `json:"component"` +} + +type cyclonedxComponent struct { + Name string `json:"name"` + Type string `json:"type"` + Version string `json:"version"` + Properties []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"properties"` +} + +type cyclonedxMetadataV15 struct { + Tools struct { + Components []struct { // available from 1.5 onwards + Name string `json:"name"` + Version string `json:"version"` + } `json:"components"` + } `json:"tools"` + Component cyclonedxComponent `json:"component"` } func NewCyclonedxJSONCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*CyclonedxJSONCrafter, error) { @@ -100,33 +118,62 @@ func (i *CyclonedxJSONCrafter) Craft(ctx context.Context, filePath string) (*api }, } - // Include the main component information if available - mainComponent, err := i.extractMainComponent(f) - if err != nil { - i.logger.Debug().Err(err).Msg("error extracting main component from sbom, skipping...") + // parse the file to extract the main information + var doc cyclonedxDoc + if err = json.Unmarshal(f, &doc); err != nil { + i.logger.Debug().Err(err).Msg("error decoding file to extract main information, skipping ...") } - // If the main component is available, include it in the material - if mainComponent != nil { - res.M.(*api.Attestation_Material_SbomArtifact).SbomArtifact.MainComponent = &api.Attestation_Material_SBOMArtifact_MainComponent{ - Name: mainComponent.name, - Kind: mainComponent.kind, - Version: mainComponent.version, + switch doc.SpecVersion { + case "1.4": + var metaV14 cyclonedxMetadataV14 + if err = json.Unmarshal(doc.Metadata, &metaV14); err != nil { + i.logger.Debug().Err(err).Msg("error decoding file to extract main information, skipping ...") + } else { + i.extractMetadata(m, &metaV14) + } + default: // 1.5 onwards + var metaV15 cyclonedxMetadataV15 + if err = json.Unmarshal(doc.Metadata, &metaV15); err != nil { + i.logger.Debug().Err(err).Msg("error decoding file to extract main information, skipping ...") + } else { + i.extractMetadata(m, &metaV15) } } return res, nil } -// extractMainComponent inspects the SBOM and extracts the main component if any and available -func (i *CyclonedxJSONCrafter) extractMainComponent(rawFile []byte) (*SBOMMainComponentInfo, error) { - var mainComponent mainComponentStruct - err := json.Unmarshal(rawFile, &mainComponent) - if err != nil { - return nil, fmt.Errorf("error extracting main component: %w", err) +func (i *CyclonedxJSONCrafter) extractMetadata(m *api.Attestation_Material, metadata any) { + m.Annotations = make(map[string]string) + + switch meta := metadata.(type) { + case *cyclonedxMetadataV14: + if err := i.extractMainComponent(m, &meta.Component); err != nil { + i.logger.Debug().Err(err).Msg("error extracting main component from sbom, skipping...") + } + + if len(meta.Tools) > 0 { + m.Annotations[AnnotationToolNameKey] = meta.Tools[0].Name + m.Annotations[AnnotationToolVersionKey] = meta.Tools[0].Version + } + case *cyclonedxMetadataV15: + if err := i.extractMainComponent(m, &meta.Component); err != nil { + i.logger.Debug().Err(err).Msg("error extracting main component from sbom, skipping...") + } + + if len(meta.Tools.Components) > 0 { + m.Annotations[AnnotationToolNameKey] = meta.Tools.Components[0].Name + m.Annotations[AnnotationToolVersionKey] = meta.Tools.Components[0].Version + } + default: + i.logger.Debug().Msg("unknown metadata version") } +} - component := mainComponent.Metadata.Component +// extractMainComponent inspects the SBOM and extracts the main component if any and available +func (i *CyclonedxJSONCrafter) extractMainComponent(m *api.Attestation_Material, component *cyclonedxComponent) error { + var mainComponent *SBOMMainComponentInfo // If the version is empty, try to extract it from the properties if component.Version == "" { @@ -141,23 +188,32 @@ func (i *CyclonedxJSONCrafter) extractMainComponent(rawFile []byte) (*SBOMMainCo } if component.Type != containerComponentKind { - return &SBOMMainComponentInfo{ + mainComponent = &SBOMMainComponentInfo{ name: component.Name, kind: component.Type, version: component.Version, - }, nil + } + } else { + // Standardize the name to have the full repository name including the registry and + // sanitize the name to remove the possible tag from the repository name + ref, err := remotename.ParseReference(component.Name) + if err != nil { + return fmt.Errorf("couldn't parse OCI image repository name: %w", err) + } + + mainComponent = &SBOMMainComponentInfo{ + name: ref.Context().String(), + kind: component.Type, + version: component.Version, + } } - // Standardize the name to have the full repository name including the registry and - // sanitize the name to remove the possible tag from the repository name - ref, err := remotename.ParseReference(component.Name) - if err != nil { - return nil, fmt.Errorf("couldn't parse OCI image repository name: %w", err) + // If the main component is available, include it in the material + m.M.(*api.Attestation_Material_SbomArtifact).SbomArtifact.MainComponent = &api.Attestation_Material_SBOMArtifact_MainComponent{ + Name: mainComponent.name, + Kind: mainComponent.kind, + Version: mainComponent.version, } - return &SBOMMainComponentInfo{ - name: ref.Context().String(), - kind: component.Type, - version: component.Version, - }, nil + return nil } diff --git a/pkg/attestation/crafter/materials/cyclonedxjson_test.go b/pkg/attestation/crafter/materials/cyclonedxjson_test.go index 466601970..95d3d949d 100644 --- a/pkg/attestation/crafter/materials/cyclonedxjson_test.go +++ b/pkg/attestation/crafter/materials/cyclonedxjson_test.go @@ -73,6 +73,7 @@ func TestCyclonedxJSONCraft(t *testing.T) { wantMainComponent string wantMainComponentKind string wantMainComponentVersion string + annotations map[string]string }{ { name: "invalid path", @@ -96,6 +97,10 @@ func TestCyclonedxJSONCraft(t *testing.T) { wantFilename: "sbom.cyclonedx.json", wantMainComponent: ".", wantMainComponentKind: "file", + annotations: map[string]string{ + "chainloop.material.tool.name": "syft", + "chainloop.material.tool.version": "0.73.0", + }, }, { name: "1.5 version", @@ -105,6 +110,10 @@ func TestCyclonedxJSONCraft(t *testing.T) { wantMainComponent: "ghcr.io/chainloop-dev/chainloop/control-plane", wantMainComponentKind: "container", wantMainComponentVersion: "v0.55.0", + annotations: map[string]string{ + "chainloop.material.tool.name": "syft", + "chainloop.material.tool.version": "0.101.1", + }, }, } @@ -151,6 +160,12 @@ func TestCyclonedxJSONCraft(t *testing.T) { }, got.GetSbomArtifact(), ) + + if tc.annotations != nil { + for k, v := range tc.annotations { + ast.Equal(v, got.Annotations[k]) + } + } }) } } diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index d71127ee3..45b4e8188 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -34,6 +34,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +const AnnotationToolNameKey = "chainloop.material.tool.name" +const AnnotationToolVersionKey = "chainloop.material.tool.version" + var ( // ErrInvalidMaterialType is returned when the provided material type // is not from the kind we are expecting @@ -232,7 +235,9 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia } m.AddedAt = timestamppb.New(time.Now()) - m.Annotations = make(map[string]string) + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } for _, annotation := range materialSchema.Annotations { m.Annotations[annotation.Name] = annotation.Value diff --git a/pkg/attestation/crafter/materials/sarif.go b/pkg/attestation/crafter/materials/sarif.go index 2cf48d849..0c64cfe15 100644 --- a/pkg/attestation/crafter/materials/sarif.go +++ b/pkg/attestation/crafter/materials/sarif.go @@ -54,5 +54,24 @@ func (i *SARIFCrafter) Craft(ctx context.Context, filepath string) (*api.Attesta return nil, fmt.Errorf("invalid SARIF file: %w", ErrInvalidMaterialType) } - return uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) + m, err := uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, doc) + + return m, nil +} + +func (i *SARIFCrafter) injectAnnotations(m *api.Attestation_Material, doc *sarif.Report) { + // add vendor information + if len(doc.Runs) > 0 { + // assuming vendor from first run. + m.Annotations = make(map[string]string) + m.Annotations[AnnotationToolNameKey] = doc.Runs[0].Tool.Driver.Name + if doc.Runs[0].Tool.Driver.Version != nil { + m.Annotations[AnnotationToolVersionKey] = *doc.Runs[0].Tool.Driver.Version + } + } } diff --git a/pkg/attestation/crafter/materials/spdxjson.go b/pkg/attestation/crafter/materials/spdxjson.go index fda808f9a..1809e134b 100644 --- a/pkg/attestation/crafter/materials/spdxjson.go +++ b/pkg/attestation/crafter/materials/spdxjson.go @@ -19,11 +19,13 @@ import ( "context" "fmt" "os" + "strings" 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" "github.com/spdx/tools-golang/json" + "github.com/spdx/tools-golang/spdx" "github.com/rs/zerolog" ) @@ -52,11 +54,36 @@ func (i *SPDXJSONCrafter) Craft(ctx context.Context, filePath string) (*api.Atte defer f.Close() // Decode the file to check it's a valid SPDX BOM - _, err = json.Read(f) + doc, err := json.Read(f) if err != nil { i.logger.Debug().Err(err).Msg("error decoding file") return nil, fmt.Errorf("invalid spdx sbom file: %w", ErrInvalidMaterialType) } - return uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, doc) + + return m, nil +} + +func (i *SPDXJSONCrafter) injectAnnotations(m *api.Attestation_Material, doc *spdx.Document) { + for _, c := range doc.CreationInfo.Creators { + if c.CreatorType == "Tool" { + m.Annotations = make(map[string]string) + m.Annotations[AnnotationToolNameKey] = c.Creator + + // try to extract the tool name and version + // e.g. "myTool-1.0.0" + parts := strings.SplitN(c.Creator, "-", 2) + if len(parts) == 2 { + m.Annotations[AnnotationToolNameKey] = parts[0] + m.Annotations[AnnotationToolVersionKey] = parts[1] + } + break + } + } }