diff --git a/app/cli/api/attestation/v1/crafting_state.go b/app/cli/api/attestation/v1/crafting_state.go index cd0899c97..5d2bd08dc 100644 --- a/app/cli/api/attestation/v1/crafting_state.go +++ b/app/cli/api/attestation/v1/crafting_state.go @@ -16,7 +16,8 @@ package v1 import ( - schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "errors" + "fmt" ) type NormalizedMaterialOutput struct { @@ -25,18 +26,25 @@ type NormalizedMaterialOutput struct { Content []byte } -func (m *Attestation_Material) NormalizedOutput() *NormalizedMaterialOutput { - switch m.MaterialType { - case schemaapi.CraftingSchema_Material_ARTIFACT, schemaapi.CraftingSchema_Material_SBOM_CYCLONEDX_JSON, schemaapi.CraftingSchema_Material_SBOM_SPDX_JSON: - a := m.GetArtifact() - return &NormalizedMaterialOutput{a.Name, a.Digest, a.IsSubject, a.Content} - case schemaapi.CraftingSchema_Material_CONTAINER_IMAGE: - a := m.GetContainerImage() - return &NormalizedMaterialOutput{a.Name, a.Digest, a.IsSubject, nil} - case schemaapi.CraftingSchema_Material_STRING: - a := m.GetString_() - return &NormalizedMaterialOutput{Content: []byte(a.Value)} +// NormalizedOutput returns a common representation of the properties of a material +// regardless of how it's been encoded. +// For example, it's common to have materials based on artifacts, so we want to normalize the output +func (m *Attestation_Material) NormalizedOutput() (*NormalizedMaterialOutput, error) { + if m == nil { + return nil, errors.New("material not provided") } - return nil + if a := m.GetContainerImage(); a != nil { + return &NormalizedMaterialOutput{a.Name, a.Digest, a.IsSubject, nil}, nil + } + + if a := m.GetString_(); a != nil { + return &NormalizedMaterialOutput{Content: []byte(a.Value)}, nil + } + + if a := m.GetArtifact(); a != nil { + return &NormalizedMaterialOutput{a.Name, a.Digest, a.IsSubject, a.Content}, nil + } + + return nil, fmt.Errorf("unknown material: %s", m.MaterialType) } diff --git a/app/cli/api/attestation/v1/crafting_state_test.go b/app/cli/api/attestation/v1/crafting_state_test.go new file mode 100644 index 000000000..40658db59 --- /dev/null +++ b/app/cli/api/attestation/v1/crafting_state_test.go @@ -0,0 +1,109 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "testing" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeOutput(t *testing.T) { + artifactBasedMaterial := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_SARIF, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "name", Digest: "deadbeef", IsSubject: true, Content: []byte("content"), + }, + }, + } + + artifactBasedMaterialWant := &NormalizedMaterialOutput{ + Name: "name", Digest: "deadbeef", IsOutput: true, Content: []byte("content"), + } + + containerMaterial := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_CONTAINER_IMAGE, + M: &Attestation_Material_ContainerImage_{ + ContainerImage: &Attestation_Material_ContainerImage{ + Name: "name", Digest: "deadbeef", IsSubject: true, + }, + }, + } + + containerMaterialWant := &NormalizedMaterialOutput{ + Name: "name", Digest: "deadbeef", IsOutput: true, + } + + keyValMaterial := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_STRING, + M: &Attestation_Material_String_{ + String_: &Attestation_Material_KeyVal{ + Id: "id", Value: "value", + }, + }, + } + + keyValWant := &NormalizedMaterialOutput{ + Content: []byte("value"), + } + + testCases := []struct { + name string + material *Attestation_Material + want *NormalizedMaterialOutput + wantErr string + }{ + { + name: "nil material", + wantErr: "material not provided", + }, + { + name: "empty material", + material: &Attestation_Material{}, + wantErr: "unknown material: MATERIAL_TYPE_UNSPECIFIED", + }, + { + name: "artifact based material", + material: artifactBasedMaterial, + want: artifactBasedMaterialWant, + }, + { + name: "Container image material", + material: containerMaterial, + want: containerMaterialWant, + }, + { + name: "keyval material", + material: keyValMaterial, + want: keyValWant, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := (tc.material).NormalizedOutput() + if tc.wantErr != "" { + assert.EqualError(t, err, tc.wantErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/attestation/renderer/chainloop/v01.go b/internal/attestation/renderer/chainloop/v01.go index 94b829e04..d38ea4f60 100644 --- a/internal/attestation/renderer/chainloop/v01.go +++ b/internal/attestation/renderer/chainloop/v01.go @@ -116,7 +116,10 @@ func outputChainloopMaterials(att *v1.Attestation, onlyOutput bool) []*Provenanc mdef := materials[mdefName] artifactType := mdef.MaterialType - nMaterial := mdef.NormalizedOutput() + nMaterial, err := mdef.NormalizedOutput() + if err != nil { + continue + } // Skip if we are expecting to show only the materials marked as output if onlyOutput && !nMaterial.IsOutput { diff --git a/internal/attestation/renderer/chainloop/v02.go b/internal/attestation/renderer/chainloop/v02.go index 9cc5e549c..cb017f11c 100644 --- a/internal/attestation/renderer/chainloop/v02.go +++ b/internal/attestation/renderer/chainloop/v02.go @@ -49,9 +49,14 @@ func NewChainloopRendererV02(att *v1.Attestation, builderVersion, builderDigest } func (r *RendererV02) Predicate() (interface{}, error) { + normalizedMaterials, err := outputMaterials(r.att, false) + if err != nil { + return nil, fmt.Errorf("error normalizing materials: %w", err) + } + return ProvenancePredicateV02{ ProvenancePredicateCommon: predicateCommon(r.builder, r.att), - Materials: outputMaterials(r.att, false), + Materials: normalizedMaterials, }, nil } @@ -71,7 +76,12 @@ func (r *RendererV02) Header() (*in_toto.StatementHeader, error) { }, } - for _, m := range outputMaterials(r.att, true) { + normalizedMaterials, err := outputMaterials(r.att, true) + if err != nil { + return nil, fmt.Errorf("error normalizing materials: %w", err) + } + + for _, m := range normalizedMaterials { if m.Digest != nil { subjects = append(subjects, in_toto.Subject{ Name: m.Name, @@ -98,7 +108,7 @@ func builtInAnnotation(name string) string { return fmt.Sprintf("%s%s", builtInAnnotationPrefix, name) } -func outputMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.ResourceDescriptor { +func outputMaterials(att *v1.Attestation, onlyOutput bool) ([]*slsa_v1.ResourceDescriptor, error) { // Sort material keys to stabilize output keys := make([]string, 0, len(att.GetMaterials())) for k := range att.GetMaterials() { @@ -113,7 +123,10 @@ func outputMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.ResourceDe mdef := materials[mdefName] artifactType := mdef.MaterialType - nMaterial := mdef.NormalizedOutput() + nMaterial, err := mdef.NormalizedOutput() + if err != nil { + return nil, fmt.Errorf("error normalizing material: %w", err) + } // Skip if we are expecting to show only the materials marked as output if onlyOutput && !nMaterial.IsOutput { @@ -157,7 +170,7 @@ func outputMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.ResourceDe res = append(res, material) } - return res + return res, nil } // Implement NormalizablePredicate interface