From 82031bdaa63025775fa9bf8915e19c31f322a059 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 8 May 2023 10:31:02 +0200 Subject: [PATCH 1/4] feat(attestation): enable predicate v2 Signed-off-by: Miguel Martinez Trivino --- .../internal/action/workflow_run_describe.go | 60 ++++-------------- .../internal/service/attestation.go | 18 +++--- .../renderer/chainloop/chainloop.go | 63 +++++++++++++++++++ .../attestation/renderer/chainloop/v01.go | 35 ++++------- .../attestation/renderer/chainloop/v02.go | 30 +++++++++ internal/attestation/renderer/renderer.go | 2 +- 6 files changed, 127 insertions(+), 81 deletions(-) diff --git a/app/cli/internal/action/workflow_run_describe.go b/app/cli/internal/action/workflow_run_describe.go index 3557c7be8..3ee346c71 100644 --- a/app/cli/internal/action/workflow_run_describe.go +++ b/app/cli/internal/action/workflow_run_describe.go @@ -17,7 +17,6 @@ package action import ( "context" - "encoding/json" "errors" "fmt" "time" @@ -43,13 +42,12 @@ type WorkflowRunItemFull struct { } type WorkflowRunAttestationItem struct { - ID string `json:"id"` - CreatedAt *time.Time `json:"createdAt"` - Envelope *dsse.Envelope `json:"envelope"` - statement *in_toto.Statement - predicateV1 *chainloop.ProvenancePredicateV01 - Materials []*Material `json:"materials,omitempty"` - EnvVars []*EnvVar `json:"envvars,omitempty"` + ID string `json:"id"` + CreatedAt *time.Time `json:"createdAt"` + Envelope *dsse.Envelope `json:"envelope"` + statement *in_toto.Statement + Materials []*Material `json:"materials,omitempty"` + EnvVars []*EnvVar `json:"envvars,omitempty"` } type Material struct { @@ -63,10 +61,6 @@ type EnvVar struct { Value string `json:"value"` } -func (i *WorkflowRunAttestationItem) Predicate() *chainloop.ProvenancePredicateV01 { - return i.predicateV1 -} - func (i *WorkflowRunAttestationItem) Statement() *in_toto.Statement { return i.statement } @@ -114,24 +108,9 @@ func (action *WorkflowRunDescribe) Run(runID string, verify bool, publicKey stri item.Verified = true } - // Decode in-toto statement - statement := &in_toto.Statement{} - decodedPayload, err := envelope.DecodeB64Payload() + statement, err := chainloop.ExtractStatement(envelope) if err != nil { - return nil, fmt.Errorf("decoding payload: %w", err) - } - - if err := json.Unmarshal(decodedPayload, statement); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - var predicate *chainloop.ProvenancePredicateV01 - if statement.PredicateType == chainloop.PredicateTypeV01 { - if predicate, err = extractPredicateV1(statement); err != nil { - return nil, fmt.Errorf("extracting predicate: %w", err) - } - } else { - return nil, errors.New("predicate type not supported") + return nil, fmt.Errorf("extracting statement: %w", err) } envVars := make([]*EnvVar, 0, len(attestation.GetEnvVars())) @@ -146,30 +125,15 @@ func (action *WorkflowRunDescribe) Run(runID string, verify bool, publicKey stri item.Attestation = &WorkflowRunAttestationItem{ ID: attestation.Id, CreatedAt: toTimePtr(attestation.CreatedAt.AsTime()), - Envelope: envelope, - statement: statement, - predicateV1: predicate, - EnvVars: envVars, - Materials: materials, + Envelope: envelope, + statement: statement, + EnvVars: envVars, + Materials: materials, } return item, nil } -func extractPredicateV1(statement *in_toto.Statement) (*chainloop.ProvenancePredicateV01, error) { - jsonPredicate, err := json.Marshal(statement.Predicate) - if err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - predicate := &chainloop.ProvenancePredicateV01{} - if err := json.Unmarshal(jsonPredicate, predicate); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - return predicate, nil -} - func verifyEnvelope(ctx context.Context, e *dsse.Envelope, publicKey string) error { // Currently we only support basic cosign public key check // TODO: Add more verification methods diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 9a27eea50..34855eeb6 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -284,18 +284,20 @@ func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) { predicates, err := chainloop.ExtractPredicate(att.Envelope) if err != nil { - return nil, err + return nil, fmt.Errorf("error extracting predicate from attestation: %w", err) } - predicate := predicates.V01 - if predicate == nil { - return nil, errors.InternalServer("invalid attestation type", "attestation does not contain a V01 predicate") + var predicate chainloop.NormalizablePredicate + if predicates.V01 != nil { + predicate = predicates.V01 + } else if predicates.V02 != nil { + predicate = predicates.V02 } return &cpAPI.AttestationItem{ Envelope: encodedAttestation, - EnvVars: extractEnvVariables(predicate.Env), - Materials: extractMaterials(predicate.Materials), + EnvVars: extractEnvVariables(predicate.GetEnvVars()), + Materials: extractMaterials(predicate.GetMaterials()), }, nil } @@ -313,10 +315,10 @@ func extractEnvVariables(in map[string]string) []*cpAPI.AttestationItem_EnvVaria return res } -func extractMaterials(in []*chainloop.ProvenanceMaterial) []*cpAPI.AttestationItem_Material { +func extractMaterials(in []*chainloop.NormalizedMaterial) []*cpAPI.AttestationItem_Material { res := make([]*cpAPI.AttestationItem_Material, 0, len(in)) for _, m := range in { - res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: m.Material.String(), Type: m.Type}) + res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: m.StringValue, Type: m.Type}) } return res } diff --git a/internal/attestation/renderer/chainloop/chainloop.go b/internal/attestation/renderer/chainloop/chainloop.go index b7de49792..2cf25ee3e 100644 --- a/internal/attestation/renderer/chainloop/chainloop.go +++ b/internal/attestation/renderer/chainloop/chainloop.go @@ -21,6 +21,7 @@ import ( "time" v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/in-toto/in-toto-golang/in_toto" slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" @@ -34,8 +35,21 @@ const chainloopBuildType = "chainloop.dev/workflowrun/v0.1" const builderIDFmt = "chainloop.dev/cli/%s@%s" +// NormalizablePredicate represents a common interface of how to extract materials and env vars +type NormalizablePredicate interface { + GetEnvVars() map[string]string + GetMaterials() []*NormalizedMaterial +} + +type NormalizedMaterial struct { + Name string + Type string + StringValue string +} + type ProvenancePredicateVersions struct { V01 *ProvenancePredicateV01 + V02 *ProvenancePredicateV02 } type ProvenancePredicateCommon struct { @@ -101,6 +115,50 @@ func getChainloopMeta(att *v1.Attestation) *Metadata { } } +func ExtractStatement(envelope *dsse.Envelope) (*in_toto.Statement, error) { + decodedPayload, err := envelope.DecodeB64Payload() + if err != nil { + return nil, err + } + + // 1 - Extract the in-toto statement + statement := &in_toto.Statement{} + if err := json.Unmarshal(decodedPayload, statement); err != nil { + return nil, fmt.Errorf("un-marshaling predicate: %w", err) + } + + return statement, nil +} + +// Extract the Chainloop attestation predicate from an encoded DSSE envelope +func ExtractPredicate(envelope *dsse.Envelope) (*ProvenancePredicateVersions, error) { + // 1 - Extract the in-toto statement + statement, err := ExtractStatement(envelope) + if err != nil { + return nil, fmt.Errorf("extracting statement: %w", err) + } + + // 2 - Extract the Chainloop predicate from the in-toto statement + switch statement.PredicateType { + case PredicateTypeV01: + var predicate *ProvenancePredicateV01 + if err = extractPredicate(statement, &predicate); err != nil { + return nil, fmt.Errorf("extracting predicate: %w", err) + } + + return &ProvenancePredicateVersions{V01: predicate}, nil + case PredicateTypeV02: + var predicate *ProvenancePredicateV02 + if err = extractPredicate(statement, &predicate); err != nil { + return nil, fmt.Errorf("extracting predicate: %w", err) + } + + return &ProvenancePredicateVersions{V02: predicate}, nil + default: + return nil, fmt.Errorf("unsupported predicate type: %s", statement.PredicateType) + } +} + func extractPredicate(statement *in_toto.Statement, v any) error { jsonPredicate, err := json.Marshal(statement.Predicate) if err != nil { @@ -113,3 +171,8 @@ func extractPredicate(statement *in_toto.Statement, v any) error { return nil } + +// Implement NormalizablePredicate interface +func (p *ProvenancePredicateCommon) GetEnvVars() map[string]string { + return p.Env +} diff --git a/internal/attestation/renderer/chainloop/v01.go b/internal/attestation/renderer/chainloop/v01.go index e6f252e2f..f9c94c1b6 100644 --- a/internal/attestation/renderer/chainloop/v01.go +++ b/internal/attestation/renderer/chainloop/v01.go @@ -24,7 +24,6 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" "github.com/in-toto/in-toto-golang/in_toto" - "github.com/secure-systems-lab/go-securesystemslib/dsse" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" @@ -165,29 +164,17 @@ func outputChainloopMaterials(att *v1.Attestation, onlyOutput bool) []*Provenanc return res } -// Extract the Chainloop attestation predicate from an encoded DSSE envelope -func ExtractPredicate(envelope *dsse.Envelope) (*ProvenancePredicateVersions, error) { - decodedPayload, err := envelope.DecodeB64Payload() - if err != nil { - return nil, err - } - - // 1 - Extract the in-toto statement - statement := &in_toto.Statement{} - if err := json.Unmarshal(decodedPayload, statement); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) +// Implement NormalizablePredicate +// Override +func (p *ProvenancePredicateV01) GetMaterials() []*NormalizedMaterial { + res := make([]*NormalizedMaterial, 0, len(p.Materials)) + for _, m := range p.Materials { + res = append(res, &NormalizedMaterial{ + Name: m.Name, + Type: m.Type, + StringValue: m.Material.String(), + }) } - // 2 - Extract the Chainloop predicate from the in-toto statement - switch statement.PredicateType { - case PredicateTypeV01: - var predicate *ProvenancePredicateV01 - if err = extractPredicate(statement, &predicate); err != nil { - return nil, fmt.Errorf("extracting predicate: %w", err) - } - - return &ProvenancePredicateVersions{V01: predicate}, nil - default: - return nil, fmt.Errorf("unsupported predicate type: %s", statement.PredicateType) - } + return res } diff --git a/internal/attestation/renderer/chainloop/v02.go b/internal/attestation/renderer/chainloop/v02.go index c392546cb..1093943b1 100644 --- a/internal/attestation/renderer/chainloop/v02.go +++ b/internal/attestation/renderer/chainloop/v02.go @@ -133,3 +133,33 @@ func outputSLSAMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.Resour return res } + +// Implement NormalizablePredicate interface +func (p *ProvenancePredicateV02) GetMaterials() []*NormalizedMaterial { + res := make([]*NormalizedMaterial, 0, len(p.Materials)) + for _, material := range p.Materials { + m := &NormalizedMaterial{} + if v, ok := material.Annotations[AnnotationMaterialType]; ok { + // Set the type + m.Type = v.(string) + // Set the Value + if m.Type == schemaapi.CraftingSchema_Material_STRING.String() { + m.StringValue = string(material.Content) + } else { + // we just care about the first one + for alg, h := range material.Digest { + m.StringValue = fmt.Sprintf("%s@%s:%s", material.Name, alg, h) + } + } + } + + // Set the Material Name + if v, ok := material.Annotations[AnnotationMaterialName]; ok { + m.Name = v.(string) + } + + res = append(res, m) + } + + return res +} diff --git a/internal/attestation/renderer/renderer.go b/internal/attestation/renderer/renderer.go index 3a54709ae..a73a0a11f 100644 --- a/internal/attestation/renderer/renderer.go +++ b/internal/attestation/renderer/renderer.go @@ -64,7 +64,7 @@ func NewAttestationRenderer(state *v1.CraftingState, keyPath, builderVersion, bu logger: zerolog.Nop(), signingKeyPath: keyPath, att: state.GetAttestation(), - renderer: chainloop.NewChainloopRendererV01(state.GetAttestation(), builderVersion, builderDigest), + renderer: chainloop.NewChainloopRendererV02(state.GetAttestation(), builderVersion, builderDigest), } for _, opt := range opts { From 22d85b6d3bdd91340e838dbd2b6394e96db4a126 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 9 May 2023 13:26:50 +0200 Subject: [PATCH 2/4] chore: enable v02 by default Signed-off-by: Miguel Martinez Trivino --- .../dependencytrack/dependencytrack.go | 22 +++---- .../internal/service/attestation.go | 19 +++--- .../renderer/chainloop/chainloop.go | 31 +++++---- .../chainloop/testdata/valid.predicate.json | 65 ------------------- .../attestation/renderer/chainloop/v01.go | 36 ++++------ .../renderer/chainloop/v01_test.go | 59 +++++++++-------- .../attestation/renderer/chainloop/v02.go | 7 +- 7 files changed, 85 insertions(+), 154 deletions(-) delete mode 100644 internal/attestation/renderer/chainloop/testdata/valid.predicate.json diff --git a/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go b/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go index 1737e64e6..229543eec 100644 --- a/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go +++ b/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go @@ -106,16 +106,11 @@ func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID st } // There is at least one enabled integration, extract the SBOMs - predicates, err := chainloop.ExtractPredicate(envelope) + predicate, err := chainloop.ExtractPredicate(envelope) if err != nil { return err } - predicate := predicates.V01 - if predicate == nil { - return errors.Forbidden("not implemented", "only v0.1 predicate is supported for now") - } - repo, err := uc.ociUC.FindMainRepo(ctx, orgID) if err != nil { return err @@ -123,26 +118,25 @@ func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID st return errors.NotFound("not found", "main repository not found") } - for _, m := range predicate.Materials { - if m.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() { + for _, material := range predicate.GetMaterials() { + if material.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() { continue } - buf := bytes.NewBuffer(nil) - digest, ok := m.Material.SLSA.Digest["sha256"] - if !ok { + if material.Hash == nil { continue } - digest = "sha256:" + digest + digest := material.Hash.String() - uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", m.Name) + uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", material.Name) // Download SBOM + buf := bytes.NewBuffer(nil) if err := uc.casClient.Download(ctx, repo.SecretName, buf, digest); err != nil { return fmt.Errorf("downloading from CAS: %w", err) } - uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", m.Name) + uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", material.Name) // Run integrations with that sbom var wg sync.WaitGroup diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 34855eeb6..71cd7764a 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -282,18 +282,11 @@ func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) { return nil, err } - predicates, err := chainloop.ExtractPredicate(att.Envelope) + predicate, err := chainloop.ExtractPredicate(att.Envelope) if err != nil { return nil, fmt.Errorf("error extracting predicate from attestation: %w", err) } - var predicate chainloop.NormalizablePredicate - if predicates.V01 != nil { - predicate = predicates.V01 - } else if predicates.V02 != nil { - predicate = predicates.V02 - } - return &cpAPI.AttestationItem{ Envelope: encodedAttestation, EnvVars: extractEnvVariables(predicate.GetEnvVars()), @@ -318,7 +311,15 @@ func extractEnvVariables(in map[string]string) []*cpAPI.AttestationItem_EnvVaria func extractMaterials(in []*chainloop.NormalizedMaterial) []*cpAPI.AttestationItem_Material { res := make([]*cpAPI.AttestationItem_Material, 0, len(in)) for _, m := range in { - res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: m.StringValue, Type: m.Type}) + // Initialize simply with the value + displayValue := m.Value + // Override if there is a hash attached + if m.Hash != nil { + displayValue = fmt.Sprintf("%s@%s", m.Value, m.Hash) + + } + + res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: displayValue, Type: m.Type}) } return res } diff --git a/internal/attestation/renderer/chainloop/chainloop.go b/internal/attestation/renderer/chainloop/chainloop.go index 2cf25ee3e..f2a375ae1 100644 --- a/internal/attestation/renderer/chainloop/chainloop.go +++ b/internal/attestation/renderer/chainloop/chainloop.go @@ -23,13 +23,11 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" + crv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" ) -// // Replace custom material type with https://github.com/in-toto/attestation/blob/main/spec/v1.0/resource_descriptor.md -// const ChainloopPredicateTypeV02 = "chainloop.dev/attestation/v0.2" - // TODO: Figure out a more appropriate meaning const chainloopBuildType = "chainloop.dev/workflowrun/v0.1" @@ -42,14 +40,14 @@ type NormalizablePredicate interface { } type NormalizedMaterial struct { - Name string - Type string - StringValue string -} - -type ProvenancePredicateVersions struct { - V01 *ProvenancePredicateV01 - V02 *ProvenancePredicateV02 + // Name of the Material + Name string + // Type of the Material + Type string + // Either the fileName or the actual string content + Value string + // Hash of the Material + Hash *crv1.Hash } type ProvenancePredicateCommon struct { @@ -131,7 +129,12 @@ func ExtractStatement(envelope *dsse.Envelope) (*in_toto.Statement, error) { } // Extract the Chainloop attestation predicate from an encoded DSSE envelope -func ExtractPredicate(envelope *dsse.Envelope) (*ProvenancePredicateVersions, error) { +// NOTE: We return a NormalizablePredicate interface to allow for future versions +// of the predicate to be extracted without updating the consumer. +// Yes, having the producer define and return an interface is an anti-pattern. +// but it greatly simplifies the code since there are multiple consumers at different layers of the app +// and we expect predicates to evolve quickly +func ExtractPredicate(envelope *dsse.Envelope) (NormalizablePredicate, error) { // 1 - Extract the in-toto statement statement, err := ExtractStatement(envelope) if err != nil { @@ -146,14 +149,14 @@ func ExtractPredicate(envelope *dsse.Envelope) (*ProvenancePredicateVersions, er return nil, fmt.Errorf("extracting predicate: %w", err) } - return &ProvenancePredicateVersions{V01: predicate}, nil + return predicate, nil case PredicateTypeV02: var predicate *ProvenancePredicateV02 if err = extractPredicate(statement, &predicate); err != nil { return nil, fmt.Errorf("extracting predicate: %w", err) } - return &ProvenancePredicateVersions{V02: predicate}, nil + return predicate, nil default: return nil, fmt.Errorf("unsupported predicate type: %s", statement.PredicateType) } diff --git a/internal/attestation/renderer/chainloop/testdata/valid.predicate.json b/internal/attestation/renderer/chainloop/testdata/valid.predicate.json deleted file mode 100644 index ac0c37881..000000000 --- a/internal/attestation/renderer/chainloop/testdata/valid.predicate.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "buildType": "chainloop.dev/workflowrun/v0.1", - "builder": { - "id": "chainloop.dev/cli/0.8.92@sha256:fa01c6cd104803ed5b1e80e216c1c7f6244b21cef85ca48bb69307ba1713e619" - }, - "env": { - "GITHUB_ACTOR": "migmartri", - "GITHUB_REF": "refs/tags/v0.0.39", - "GITHUB_REPOSITORY": "chainloop-dev/integration-demo", - "GITHUB_REPOSITORY_OWNER": "chainloop-dev", - "GITHUB_RUN_ID": "4410543365", - "GITHUB_SHA": "0accc9392fb1f9b258167c18ffa0aeb626973f1c", - "RUNNER_NAME": "Hosted Agent", - "RUNNER_OS": "Linux" - }, - "materials": [ - { - "material": { - "slsa": { - "digest": { - "sha256": "b155cdfc328b273c4b741c08b3b84ac441b0562ca51893f23495b35abf89ea87" - }, - "uri": "integration-demo_0.0.39_linux_amd64.tar.gz" - } - }, - "name": "binary", - "type": "ARTIFACT" - }, - { - "material": { - "slsa": { - "digest": { - "sha256": "e0d8179991dd735baf0961901b33476a76a0f300bc4ea07e3d7ae7c24e147193" - }, - "uri": "ghcr.io/chainloop-dev/integration-demo" - } - }, - "name": "image", - "type": "CONTAINER_IMAGE" - }, - { - "material": { - "slsa": { - "digest": { - "sha256": "b50f38961cc2e97d0903f4683a40e2528f7f6c9d382e8c6048b0363af95b7080" - }, - "uri": "sbom.cyclonedx.json" - } - }, - "name": "sbom", - "type": "SBOM_CYCLONEDX_JSON" - } - ], - "metadata": { - "finishedAt": "2023-03-13T23:36:36.749325103Z", - "initializedAt": "2023-03-13T23:35:36.983138919Z", - "name": "build-and-release", - "project": "integration-demo", - "team": "", - "workflowID": "37022b2f-34c3-4f47-9fd7-514b4a7baaad", - "workflowRunID": "91093f6e-37c0-4e02-af5d-c9859596d125" - }, - "runnerType": "GITHUB_ACTION", - "runnerURL": "https://github.com/chainloop-dev/integration-demo/actions/runs/4410543365" -} \ No newline at end of file diff --git a/internal/attestation/renderer/chainloop/v01.go b/internal/attestation/renderer/chainloop/v01.go index f9c94c1b6..894062ef5 100644 --- a/internal/attestation/renderer/chainloop/v01.go +++ b/internal/attestation/renderer/chainloop/v01.go @@ -26,6 +26,7 @@ import ( "github.com/in-toto/in-toto-golang/in_toto" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + crv1 "github.com/google/go-containerregistry/pkg/v1" slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" ) @@ -46,23 +47,6 @@ type SLSACommonProvenanceMaterial struct { *slsacommon.ProvenanceMaterial } -func (m *SLSACommonProvenanceMaterial) String() (res string) { - // we just care about the first one - for alg, h := range m.Digest { - res = fmt.Sprintf("%s@%s:%s", m.URI, alg, h) - } - - return -} - -func (m *ProvenanceM) String() string { - if m.SLSA != nil { - return m.SLSA.String() - } - - return m.StringVal -} - type ProvenanceM struct { SLSA *SLSACommonProvenanceMaterial `json:"slsa,omitempty"` StringVal string `json:"stringVal,omitempty"` @@ -169,11 +153,19 @@ func outputChainloopMaterials(att *v1.Attestation, onlyOutput bool) []*Provenanc func (p *ProvenancePredicateV01) GetMaterials() []*NormalizedMaterial { res := make([]*NormalizedMaterial, 0, len(p.Materials)) for _, m := range p.Materials { - res = append(res, &NormalizedMaterial{ - Name: m.Name, - Type: m.Type, - StringValue: m.Material.String(), - }) + nm := &NormalizedMaterial{ + Name: m.Name, + Type: m.Type, + } + + if m.Material.StringVal != "" { + nm.Value = m.Material.StringVal + } else if m.Material.SLSA != nil { + nm.Value = m.Material.SLSA.URI + nm.Hash = &crv1.Hash{Algorithm: "sha256", Hex: m.Material.SLSA.Digest["sha256"]} + } + + res = append(res, nm) } return res diff --git a/internal/attestation/renderer/chainloop/v01_test.go b/internal/attestation/renderer/chainloop/v01_test.go index da6978eec..532b3afd1 100644 --- a/internal/attestation/renderer/chainloop/v01_test.go +++ b/internal/attestation/renderer/chainloop/v01_test.go @@ -17,11 +17,11 @@ package chainloop import ( "encoding/json" - "fmt" "os" "testing" api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + crv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" @@ -85,16 +85,33 @@ func TestRenderV01(t *testing.T) { func TestExtractPredicate(t *testing.T) { testCases := []struct { - name string - envelopePath string - predicatePath string - wantErr bool + name string + envelopePath string + envVars map[string]string + materials []*NormalizedMaterial + wantErr bool }{ { - name: "valid envelope", - envelopePath: "testdata/valid.envelope.json", - predicatePath: "testdata/valid.predicate.json", - wantErr: false, + name: "valid envelope", + envelopePath: "testdata/valid.envelope.json", + envVars: map[string]string{"GITHUB_ACTOR": "migmartri", "GITHUB_REF": "refs/tags/v0.0.39", "GITHUB_REPOSITORY": "chainloop-dev/integration-demo", "GITHUB_REPOSITORY_OWNER": "chainloop-dev", "GITHUB_RUN_ID": "4410543365", "GITHUB_SHA": "0accc9392fb1f9b258167c18ffa0aeb626973f1c", "RUNNER_NAME": "Hosted Agent", "RUNNER_OS": "Linux"}, + materials: []*NormalizedMaterial{ + { + Name: "binary", Type: "ARTIFACT", + Value: "integration-demo_0.0.39_linux_amd64.tar.gz", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b155cdfc328b273c4b741c08b3b84ac441b0562ca51893f23495b35abf89ea87"}, + }, + { + Name: "image", Type: "CONTAINER_IMAGE", + Value: "ghcr.io/chainloop-dev/integration-demo", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "e0d8179991dd735baf0961901b33476a76a0f300bc4ea07e3d7ae7c24e147193"}, + }, + { + Name: "sbom", Type: "SBOM_CYCLONEDX_JSON", + Value: "sbom.cyclonedx.json", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b50f38961cc2e97d0903f4683a40e2528f7f6c9d382e8c6048b0363af95b7080"}, + }, + }, }, { name: "unknown source attestation", @@ -108,17 +125,17 @@ func TestExtractPredicate(t *testing.T) { envelope, err := testEnvelope(tc.envelopePath) require.NoError(t, err) - versions, err := ExtractPredicate(envelope) + gotPredicate, err := ExtractPredicate(envelope) if tc.wantErr { assert.Error(t, err) return } - want, err := testPredicate(tc.predicatePath) - require.NoError(t, err) + gotVars := gotPredicate.GetEnvVars() + assert.Equal(t, tc.envVars, gotVars) - assert.NoError(t, err) - assert.Equal(t, want, versions.V01) + gotMaterials := gotPredicate.GetMaterials() + assert.Equal(t, tc.materials, gotMaterials) }) } } @@ -137,17 +154,3 @@ func testEnvelope(filePath string) (*dsse.Envelope, error) { return &envelope, nil } - -func testPredicate(path string) (*ProvenancePredicateV01, error) { - var predicate ProvenancePredicateV01 - content, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(content, &predicate); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - return &predicate, nil -} diff --git a/internal/attestation/renderer/chainloop/v02.go b/internal/attestation/renderer/chainloop/v02.go index 1093943b1..5a0fe8e96 100644 --- a/internal/attestation/renderer/chainloop/v02.go +++ b/internal/attestation/renderer/chainloop/v02.go @@ -23,12 +23,14 @@ import ( "strings" v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + crv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" slsa_v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" ) +// Replace custom material type with https://github.com/in-toto/attestation/blob/main/spec/v1.0/resource_descriptor.md const PredicateTypeV02 = "chainloop.dev/attestation/v0.2" type ProvenancePredicateV02 struct { @@ -144,11 +146,12 @@ func (p *ProvenancePredicateV02) GetMaterials() []*NormalizedMaterial { m.Type = v.(string) // Set the Value if m.Type == schemaapi.CraftingSchema_Material_STRING.String() { - m.StringValue = string(material.Content) + m.Value = string(material.Content) } else { // we just care about the first one for alg, h := range material.Digest { - m.StringValue = fmt.Sprintf("%s@%s:%s", material.Name, alg, h) + m.Value = material.Name + m.Hash = &crv1.Hash{Algorithm: alg, Hex: h} } } } From e095cbb49243fe9179e1912f0f92d45ae994d286 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 9 May 2023 14:43:17 +0200 Subject: [PATCH 3/4] chore: add tests Signed-off-by: Miguel Martinez Trivino --- .../dependencytrack/dependencytrack.go | 1 + .../internal/service/attestation.go | 2 +- .../internal/service/attestation_test.go | 70 ++++++++++ .../renderer/chainloop/chainloop_test.go | 125 ++++++++++++++++++ ...d.envelope.json => valid.envelope.v1.json} | 0 .../chainloop/testdata/valid.envelope.v2.json | 10 ++ .../renderer/chainloop/v01_test.go | 74 ----------- .../attestation/renderer/chainloop/v02.go | 76 ++++++++--- .../renderer/chainloop/v02_test.go | 120 +++++++++++++++++ 9 files changed, 381 insertions(+), 97 deletions(-) create mode 100644 app/controlplane/internal/service/attestation_test.go create mode 100644 internal/attestation/renderer/chainloop/chainloop_test.go rename internal/attestation/renderer/chainloop/testdata/{valid.envelope.json => valid.envelope.v1.json} (100%) create mode 100644 internal/attestation/renderer/chainloop/testdata/valid.envelope.v2.json diff --git a/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go b/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go index 229543eec..fbfe88454 100644 --- a/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go +++ b/app/controlplane/internal/biz/integration/dependencytrack/dependencytrack.go @@ -124,6 +124,7 @@ func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID st } if material.Hash == nil { + uc.log.Warnw("msg", "CYCLONE_DX material but download digest missing, skipping", "workflowID", workflowID, "integration", Kind, "name", material.Name) continue } diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 71cd7764a..b8d0b9b8f 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -316,10 +316,10 @@ func extractMaterials(in []*chainloop.NormalizedMaterial) []*cpAPI.AttestationIt // Override if there is a hash attached if m.Hash != nil { displayValue = fmt.Sprintf("%s@%s", m.Value, m.Hash) - } res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: displayValue, Type: m.Type}) } + return res } diff --git a/app/controlplane/internal/service/attestation_test.go b/app/controlplane/internal/service/attestation_test.go new file mode 100644 index 000000000..3b02a9030 --- /dev/null +++ b/app/controlplane/internal/service/attestation_test.go @@ -0,0 +1,70 @@ +// +// 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 service + +import ( + "testing" + + cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop" + crv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" +) + +func TestExtractMaterials(t *testing.T) { + testCases := []struct { + name string + input []*chainloop.NormalizedMaterial + want []*cpAPI.AttestationItem_Material + }{ + { + name: "different material types", + input: []*chainloop.NormalizedMaterial{ + { + Name: "foo", + Type: "STRING", + Value: "bar", + }, + { + Name: "foo", + Type: "ARTIFACT", + Value: "bar", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, + }, + }, + want: []*cpAPI.AttestationItem_Material{ + { + Name: "foo", + Type: "STRING", + Value: "bar", + }, + { + Name: "foo", + Type: "ARTIFACT", + Value: "bar@sha256:deadbeef", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := extractMaterials(tc.input) + assert.Equal(t, got, tc.want) + }) + } + +} diff --git a/internal/attestation/renderer/chainloop/chainloop_test.go b/internal/attestation/renderer/chainloop/chainloop_test.go new file mode 100644 index 000000000..b5f2fb23a --- /dev/null +++ b/internal/attestation/renderer/chainloop/chainloop_test.go @@ -0,0 +1,125 @@ +// +// 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 chainloop + +import ( + "encoding/json" + "os" + "testing" + + crv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtractPredicate(t *testing.T) { + testCases := []struct { + name string + envelopePath string + envVars map[string]string + materials []*NormalizedMaterial + wantErr bool + }{ + { + name: "valid envelope v1", + envelopePath: "testdata/valid.envelope.v1.json", + envVars: map[string]string{"GITHUB_ACTOR": "migmartri", "GITHUB_REF": "refs/tags/v0.0.39", "GITHUB_REPOSITORY": "chainloop-dev/integration-demo", "GITHUB_REPOSITORY_OWNER": "chainloop-dev", "GITHUB_RUN_ID": "4410543365", "GITHUB_SHA": "0accc9392fb1f9b258167c18ffa0aeb626973f1c", "RUNNER_NAME": "Hosted Agent", "RUNNER_OS": "Linux"}, + materials: []*NormalizedMaterial{ + { + Name: "binary", Type: "ARTIFACT", + Value: "integration-demo_0.0.39_linux_amd64.tar.gz", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b155cdfc328b273c4b741c08b3b84ac441b0562ca51893f23495b35abf89ea87"}, + }, + { + Name: "image", Type: "CONTAINER_IMAGE", + Value: "ghcr.io/chainloop-dev/integration-demo", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "e0d8179991dd735baf0961901b33476a76a0f300bc4ea07e3d7ae7c24e147193"}, + }, + { + Name: "sbom", Type: "SBOM_CYCLONEDX_JSON", + Value: "sbom.cyclonedx.json", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b50f38961cc2e97d0903f4683a40e2528f7f6c9d382e8c6048b0363af95b7080"}, + }, + }, + }, + { + name: "valid envelope v2", + envelopePath: "testdata/valid.envelope.v2.json", + envVars: map[string]string{"CUSTOM_VAR": "foobar"}, + materials: []*NormalizedMaterial{ + { + Name: "binary", Type: "ARTIFACT", + Value: "main.go", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "8fce0203a4efaac3b08ee3ad769233039faa762a3da0777c45b315f398f0c150"}, + }, + { + Name: "image", Type: "CONTAINER_IMAGE", + Value: "index.docker.io/bitnami/nginx", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "747ef335ea27a2faf08aa292a5bc5491aff50c6a94ee4ebcbbcd43cdeccccaf1"}, + }, + { + Name: "sbom", Type: "SBOM_CYCLONEDX_JSON", + Value: "sbom.cyclonedx.json", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c"}, + }, + { + Name: "stringvar", Type: "STRING", + Value: "helloworld", + }, + }, + }, + { + name: "unknown source attestation", + envelopePath: "testdata/unknown.envelope.json", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + envelope, err := testEnvelope(tc.envelopePath) + require.NoError(t, err) + + gotPredicate, err := ExtractPredicate(envelope) + if tc.wantErr { + assert.Error(t, err) + return + } + + gotVars := gotPredicate.GetEnvVars() + assert.Equal(t, tc.envVars, gotVars) + + gotMaterials := gotPredicate.GetMaterials() + assert.Equal(t, tc.materials, gotMaterials) + }) + } +} + +func testEnvelope(filePath string) (*dsse.Envelope, error) { + var envelope dsse.Envelope + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + err = json.Unmarshal(content, &envelope) + if err != nil { + return nil, err + } + + return &envelope, nil +} diff --git a/internal/attestation/renderer/chainloop/testdata/valid.envelope.json b/internal/attestation/renderer/chainloop/testdata/valid.envelope.v1.json similarity index 100% rename from internal/attestation/renderer/chainloop/testdata/valid.envelope.json rename to internal/attestation/renderer/chainloop/testdata/valid.envelope.v1.json diff --git a/internal/attestation/renderer/chainloop/testdata/valid.envelope.v2.json b/internal/attestation/renderer/chainloop/testdata/valid.envelope.v2.json new file mode 100644 index 000000000..dbbf1317c --- /dev/null +++ b/internal/attestation/renderer/chainloop/testdata/valid.envelope.v2.json @@ -0,0 +1,10 @@ +{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjaGFpbmxvb3AuZGV2L2F0dGVzdGF0aW9uL3YwLjIiLCJzdWJqZWN0IjpbeyJuYW1lIjoiY2hhaW5sb29wLmRldi93b3JrZmxvdy9vbGQiLCJkaWdlc3QiOnsic2hhMjU2IjoiMDQwN2EyYjAxZmJkODk4YmNjMTEwNjVmNzYxMDhkMTY5OTg2MjA2MzBmZmM0NTNkMjE2MmNmNTc5YjQyZGUwMyJ9fSx7Im5hbWUiOiJpbmRleC5kb2NrZXIuaW8vYml0bmFtaS9uZ2lueCIsImRpZ2VzdCI6eyJzaGEyNTYiOiI3NDdlZjMzNWVhMjdhMmZhZjA4YWEyOTJhNWJjNTQ5MWFmZjUwYzZhOTRlZTRlYmNiYmNkNDNjZGVjY2NjYWYxIn19XSwicHJlZGljYXRlIjp7Im1ldGFkYXRhIjp7Im5hbWUiOiJvbGQiLCJwcm9qZWN0Ijoib2xkIiwidGVhbSI6IiIsImluaXRpYWxpemVkQXQiOiIyMDIzLTA1LTA5VDExOjUxOjQwLjI3NzU2NzQ4NFoiLCJmaW5pc2hlZEF0IjoiMjAyMy0wNS0wOVQxMzo1MjoxNC4zMzc3NjgwODcrMDI6MDAiLCJ3b3JrZmxvd1J1bklEIjoiMDAzYzZkNGYtMTkwNy00NWE3LWE1OTYtNTdjMzhlZjFmZGMzIiwid29ya2Zsb3dJRCI6ImYyZmI1MzM4LWY3OWItNDRiOC05YTYxLTYxNmY1MTBjMDE5YiJ9LCJidWlsZGVyIjp7ImlkIjoiY2hhaW5sb29wLmRldi9jbGkvZGV2QHNoYTI1Njo1ODkyZWUwYTNhZjJhMDU3NzAzZjM2YmM0ZDY4MjQxMGQzNzA5YTgzOTNjMGIwYjc5MTYyOTUxZTFjZDVlZWJiIn0sImJ1aWxkVHlwZSI6ImNoYWlubG9vcC5kZXYvd29ya2Zsb3dydW4vdjAuMSIsImVudiI6eyJDVVNUT01fVkFSIjoiZm9vYmFyIn0sInJ1bm5lclR5cGUiOiJSVU5ORVJfVFlQRV9VTlNQRUNJRklFRCIsIm1hdGVyaWFscyI6W3siZGlnZXN0Ijp7InNoYTI1NiI6IjhmY2UwMjAzYTRlZmFhYzNiMDhlZTNhZDc2OTIzMzAzOWZhYTc2MmEzZGEwNzc3YzQ1YjMxNWYzOThmMGMxNTAifSwibmFtZSI6Im1haW4uZ28iLCJhbm5vdGF0aW9ucyI6eyJjaGFpbmxvb3AubWF0ZXJpYWwubmFtZSI6ImJpbmFyeSIsImNoYWlubG9vcC5tYXRlcmlhbC50eXBlIjoiQVJUSUZBQ1QifX0seyJkaWdlc3QiOnsic2hhMjU2IjoiNzQ3ZWYzMzVlYTI3YTJmYWYwOGFhMjkyYTViYzU0OTFhZmY1MGM2YTk0ZWU0ZWJjYmJjZDQzY2RlY2NjY2FmMSJ9LCJuYW1lIjoiaW5kZXguZG9ja2VyLmlvL2JpdG5hbWkvbmdpbngiLCJhbm5vdGF0aW9ucyI6eyJjaGFpbmxvb3AubWF0ZXJpYWwubmFtZSI6ImltYWdlIiwiY2hhaW5sb29wLm1hdGVyaWFsLnR5cGUiOiJDT05UQUlORVJfSU1BR0UifX0seyJkaWdlc3QiOnsic2hhMjU2IjoiMTYxNTliYjg4MWViNGFiN2ViNWQ4YWZjNTM1MGIwZmVlZWQxZTMxYzBhMjY4ZTM1NWU3NGY5Y2NiZTg4NWUwYyJ9LCJuYW1lIjoic2JvbS5jeWNsb25lZHguanNvbiIsImFubm90YXRpb25zIjp7ImNoYWlubG9vcC5tYXRlcmlhbC5uYW1lIjoic2JvbSIsImNoYWlubG9vcC5tYXRlcmlhbC50eXBlIjoiU0JPTV9DWUNMT05FRFhfSlNPTiJ9fSx7ImNvbnRlbnQiOiJhR1ZzYkc5M2IzSnNaQT09IiwiYW5ub3RhdGlvbnMiOnsiY2hhaW5sb29wLm1hdGVyaWFsLm5hbWUiOiJzdHJpbmd2YXIiLCJjaGFpbmxvb3AubWF0ZXJpYWwudHlwZSI6IlNUUklORyJ9fV19fQ==", + "signatures": [ + { + "keyid": "", + "sig": "MEUCID/MutUbBzlV5hcvc/tR6MXIXsKcakGCwoPhT9uTWPVpAiEAknjS93qTArLC7MXJ2l4q6jepiuwFFxT31hDT6OoBPUI=" + } + ] +} diff --git a/internal/attestation/renderer/chainloop/v01_test.go b/internal/attestation/renderer/chainloop/v01_test.go index 532b3afd1..94bbd9c80 100644 --- a/internal/attestation/renderer/chainloop/v01_test.go +++ b/internal/attestation/renderer/chainloop/v01_test.go @@ -21,9 +21,7 @@ import ( "testing" api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" - crv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" - "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" @@ -82,75 +80,3 @@ func TestRenderV01(t *testing.T) { }) } } - -func TestExtractPredicate(t *testing.T) { - testCases := []struct { - name string - envelopePath string - envVars map[string]string - materials []*NormalizedMaterial - wantErr bool - }{ - { - name: "valid envelope", - envelopePath: "testdata/valid.envelope.json", - envVars: map[string]string{"GITHUB_ACTOR": "migmartri", "GITHUB_REF": "refs/tags/v0.0.39", "GITHUB_REPOSITORY": "chainloop-dev/integration-demo", "GITHUB_REPOSITORY_OWNER": "chainloop-dev", "GITHUB_RUN_ID": "4410543365", "GITHUB_SHA": "0accc9392fb1f9b258167c18ffa0aeb626973f1c", "RUNNER_NAME": "Hosted Agent", "RUNNER_OS": "Linux"}, - materials: []*NormalizedMaterial{ - { - Name: "binary", Type: "ARTIFACT", - Value: "integration-demo_0.0.39_linux_amd64.tar.gz", - Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b155cdfc328b273c4b741c08b3b84ac441b0562ca51893f23495b35abf89ea87"}, - }, - { - Name: "image", Type: "CONTAINER_IMAGE", - Value: "ghcr.io/chainloop-dev/integration-demo", - Hash: &crv1.Hash{Algorithm: "sha256", Hex: "e0d8179991dd735baf0961901b33476a76a0f300bc4ea07e3d7ae7c24e147193"}, - }, - { - Name: "sbom", Type: "SBOM_CYCLONEDX_JSON", - Value: "sbom.cyclonedx.json", - Hash: &crv1.Hash{Algorithm: "sha256", Hex: "b50f38961cc2e97d0903f4683a40e2528f7f6c9d382e8c6048b0363af95b7080"}, - }, - }, - }, - { - name: "unknown source attestation", - envelopePath: "testdata/unknown.envelope.json", - wantErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - envelope, err := testEnvelope(tc.envelopePath) - require.NoError(t, err) - - gotPredicate, err := ExtractPredicate(envelope) - if tc.wantErr { - assert.Error(t, err) - return - } - - gotVars := gotPredicate.GetEnvVars() - assert.Equal(t, tc.envVars, gotVars) - - gotMaterials := gotPredicate.GetMaterials() - assert.Equal(t, tc.materials, gotMaterials) - }) - } -} - -func testEnvelope(filePath string) (*dsse.Envelope, error) { - var envelope dsse.Envelope - content, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - - err = json.Unmarshal(content, &envelope) - if err != nil { - return nil, err - } - - return &envelope, nil -} diff --git a/internal/attestation/renderer/chainloop/v02.go b/internal/attestation/renderer/chainloop/v02.go index 5a0fe8e96..0ca41e18e 100644 --- a/internal/attestation/renderer/chainloop/v02.go +++ b/internal/attestation/renderer/chainloop/v02.go @@ -51,7 +51,7 @@ func NewChainloopRendererV02(att *v1.Attestation, builderVersion, builderDigest func (r *RendererV02) Predicate() (interface{}, error) { return ProvenancePredicateV02{ ProvenancePredicateCommon: predicateCommon(r.builder, r.att), - Materials: outputSLSAMaterials(r.att, false), + Materials: outputMaterials(r.att, false), }, nil } @@ -71,7 +71,7 @@ func (r *RendererV02) Header() (*in_toto.StatementHeader, error) { }, } - for _, m := range outputSLSAMaterials(r.att, true) { + for _, m := range outputMaterials(r.att, true) { if m.Digest != nil { subjects = append(subjects, in_toto.Subject{ Name: m.Name, @@ -90,7 +90,7 @@ func (r *RendererV02) Header() (*in_toto.StatementHeader, error) { const AnnotationMaterialType = "chainloop.material.type" const AnnotationMaterialName = "chainloop.material.name" -func outputSLSAMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.ResourceDescriptor { +func outputMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.ResourceDescriptor { // Sort material keys to stabilize output keys := make([]string, 0, len(att.GetMaterials())) for k := range att.GetMaterials() { @@ -140,25 +140,9 @@ func outputSLSAMaterials(att *v1.Attestation, onlyOutput bool) []*slsa_v1.Resour func (p *ProvenancePredicateV02) GetMaterials() []*NormalizedMaterial { res := make([]*NormalizedMaterial, 0, len(p.Materials)) for _, material := range p.Materials { - m := &NormalizedMaterial{} - if v, ok := material.Annotations[AnnotationMaterialType]; ok { - // Set the type - m.Type = v.(string) - // Set the Value - if m.Type == schemaapi.CraftingSchema_Material_STRING.String() { - m.Value = string(material.Content) - } else { - // we just care about the first one - for alg, h := range material.Digest { - m.Value = material.Name - m.Hash = &crv1.Hash{Algorithm: alg, Hex: h} - } - } - } - - // Set the Material Name - if v, ok := material.Annotations[AnnotationMaterialName]; ok { - m.Name = v.(string) + m, err := normalizeMaterial(material) + if err != nil { + continue } res = append(res, m) @@ -166,3 +150,51 @@ func (p *ProvenancePredicateV02) GetMaterials() []*NormalizedMaterial { return res } + +// Translate a ResourceDescriptor to a NormalizedMaterial +func normalizeMaterial(material *slsa_v1.ResourceDescriptor) (*NormalizedMaterial, error) { + m := &NormalizedMaterial{} + + mType, ok := material.Annotations[AnnotationMaterialType] + if !ok { + return nil, fmt.Errorf("material type not found") + } + + // Set the type + m.Type = mType.(string) + + mName, ok := material.Annotations[AnnotationMaterialName] + if !ok { + return nil, fmt.Errorf("material name not found") + } + + // Set the Material Name + m.Name = mName.(string) + + // Set the Value + // If we have a string material, we just set the value + if m.Type == schemaapi.CraftingSchema_Material_STRING.String() { + if material.Content == nil { + return nil, fmt.Errorf("material content not found") + } + + m.Value = string(material.Content) + + return m, nil + } + + // for the rest of the materials we use both the name and the digest + d, ok := material.Digest["sha256"] + if !ok { + return nil, fmt.Errorf("material digest not found") + } + + m.Hash = &crv1.Hash{Algorithm: "sha256", Hex: d} + if material.Name == "" { + return nil, fmt.Errorf("material name not found") + } + + m.Value = material.Name + + return m, nil +} diff --git a/internal/attestation/renderer/chainloop/v02_test.go b/internal/attestation/renderer/chainloop/v02_test.go index 08f3e7d85..9fdbc2173 100644 --- a/internal/attestation/renderer/chainloop/v02_test.go +++ b/internal/attestation/renderer/chainloop/v02_test.go @@ -21,7 +21,9 @@ import ( "testing" api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + crv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" + slsa_v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" @@ -80,3 +82,121 @@ func TestRenderV02(t *testing.T) { }) } } + +func TestNormalizeMaterial(t *testing.T) { + testCases := []struct { + name string + input *slsa_v1.ResourceDescriptor + want *NormalizedMaterial + wantErr bool + }{ + { + name: "invalid material type", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "INVALID", + }, + }, + wantErr: true, + }, + { + name: "missing material type", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + }, + }, + wantErr: true, + }, + { + name: "missing material name", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.type": "STRING", + }, + }, + wantErr: true, + }, + { + name: "valid string material", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "STRING", + }, + Content: []byte("bar"), + }, + want: &NormalizedMaterial{ + Name: "foo", + Type: "STRING", + Value: "bar", + }, + }, + { + name: "empty string material", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "STRING", + }, + }, + wantErr: true, + }, + { + name: "valid artifact material", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "ARTIFACT", + }, + Digest: map[string]string{ + "sha256": "deadbeef", + }, + Name: "artifact.tgz", + }, + want: &NormalizedMaterial{ + Name: "foo", + Type: "ARTIFACT", + Value: "artifact.tgz", + Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"}, + }, + }, + { + name: "invalid artifact material, missing file name", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "ARTIFACT", + }, + Digest: map[string]string{ + "sha256": "deadbeef", + }, + }, + wantErr: true, + }, + { + name: "invalid artifact material, missing digest", + input: &slsa_v1.ResourceDescriptor{ + Annotations: map[string]interface{}{ + "chainloop.material.name": "foo", + "chainloop.material.type": "ARTIFACT", + }, + Name: "artifact.tgz", + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := normalizeMaterial(tc.input) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tc.want, got) + } + }) + } +} From 1933403f5937a9e7c058cba8fdca8abd8c2abb5c Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 9 May 2023 14:59:08 +0200 Subject: [PATCH 4/4] chore: add tests Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/service/attestation_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controlplane/internal/service/attestation_test.go b/app/controlplane/internal/service/attestation_test.go index 3b02a9030..38b38ad3e 100644 --- a/app/controlplane/internal/service/attestation_test.go +++ b/app/controlplane/internal/service/attestation_test.go @@ -66,5 +66,4 @@ func TestExtractMaterials(t *testing.T) { assert.Equal(t, got, tc.want) }) } - }