Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/controlplane/plugins/sdk/v1/testdata/attestations/full.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@
| Type | CONTAINER_IMAGE |
| Value | index.docker.io/bitnami/nginx |
| Digest | sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61 |
| Annotations | ------ |
| | chainloop.material.name: image |
| | chainloop.material.type: CONTAINER_IMAGE |
| Name | skynet-sbom |
| Type | SBOM_CYCLONEDX_JSON |
| Value | sbom.cyclonedx.json |
| Digest | sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c |
| Annotations | ------ |
| | chainloop.material.cas: true |
| | chainloop.material.name: skynet-sbom |
| | chainloop.material.type: SBOM_CYCLONEDX_JSON |
| | component: nginx |
| Name | skynet2-sbom |
| Type | SBOM_CYCLONEDX_JSON |
| Value | sbom.cyclonedx.json |
| Digest | sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c |
| Annotations | ------ |
| | chainloop.material.cas: true |
| | chainloop.material.name: skynet2-sbom |
| | chainloop.material.type: SBOM_CYCLONEDX_JSON |
# Environment Variables
| Name | Value |
| --- | --- |
Expand Down
10 changes: 10 additions & 0 deletions app/controlplane/plugins/sdk/v1/testdata/attestations/full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,28 @@
│ Type │ CONTAINER_IMAGE │
│ Value │ index.docker.io/bitnami/nginx │
│ Digest │ sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61 │
│ Annotations │ ------ │
│ │ chainloop.material.name: image │
│ │ chainloop.material.type: CONTAINER_IMAGE │
├─────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Name │ skynet-sbom │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Value │ sbom.cyclonedx.json │
│ Digest │ sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c │
│ Annotations │ ------ │
│ │ chainloop.material.cas: true │
│ │ chainloop.material.name: skynet-sbom │
│ │ chainloop.material.type: SBOM_CYCLONEDX_JSON │
│ │ component: nginx │
├─────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Name │ skynet2-sbom │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Value │ sbom.cyclonedx.json │
│ Digest │ sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c │
│ Annotations │ ------ │
│ │ chainloop.material.cas: true │
│ │ chainloop.material.name: skynet2-sbom │
│ │ chainloop.material.type: SBOM_CYCLONEDX_JSON │
└─────────────┴─────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────┐
│ Environment Variables │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
│ Type │ CONTAINER_IMAGE │
│ Value │ index.docker.io/bitnami/nginx │
│ Digest │ sha256:264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61 │
│ Annotations │ ------ │
│ │ chainloop.material.name: image │
│ │ chainloop.material.type: CONTAINER_IMAGE │
├─────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Name │ skynet-sbom │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Value │ sbom.cyclonedx.json │
│ Digest │ sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c │
│ Annotations │ -----... (truncated)
│ Type │ SBOM_... (truncated)

Get Full Attestation

Expand Down
6 changes: 4 additions & 2 deletions pkg/attestation/renderer/chainloop/chainloop.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024-2025 The Chainloop Authors.
// Copyright 2024-2026 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.
Expand Down Expand Up @@ -75,7 +75,9 @@ type NormalizedMaterial struct {
UploadedToCAS bool
// Whether the Material was embedded inline in the attestation
EmbeddedInline bool
// Custom annotations
// Annotations contains all material annotations, both user-defined and
// internal chainloop.* system annotations (e.g. chainloop.material.type,
// chainloop.material.cas, chainloop.material.size, etc.)
Annotations map[string]string
// Referenced source component, for SBOMs, SARIF files, etc
ReferencedSourceComponent *ReferencedSourceComponent
Expand Down
38 changes: 29 additions & 9 deletions pkg/attestation/renderer/chainloop/chainloop_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2026 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.
Expand Down Expand Up @@ -44,33 +44,53 @@ func TestExtractPredicate(t *testing.T) {
Filename: "main.go",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "8fce0203a4efaac3b08ee3ad769233039faa762a3da0777c45b315f398f0c150"},
UploadedToCAS: true,
Annotations: map[string]string{"annotation": "baz"},
Annotations: map[string]string{
"annotation": "baz",
"chainloop.material.cas": "true",
"chainloop.material.name": "binary",
"chainloop.material.type": "ARTIFACT",
},
},
{
Name: "image", Type: "CONTAINER_IMAGE",
Value: "index.docker.io/bitnami/nginx",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "747ef335ea27a2faf08aa292a5bc5491aff50c6a94ee4ebcbbcd43cdeccccaf1"},
Annotations: map[string]string{"another_annotation": "foo"},
Value: "index.docker.io/bitnami/nginx",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "747ef335ea27a2faf08aa292a5bc5491aff50c6a94ee4ebcbbcd43cdeccccaf1"},
Annotations: map[string]string{
"another_annotation": "foo",
"chainloop.material.name": "image",
"chainloop.material.type": "CONTAINER_IMAGE",
},
},
{
Name: "sbom", Type: "SBOM_CYCLONEDX_JSON",
Filename: "sbom.cyclonedx.json",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c"},
UploadedToCAS: true,
Annotations: make(map[string]string),
Annotations: map[string]string{
"chainloop.material.cas": "true",
"chainloop.material.name": "sbom",
"chainloop.material.type": "SBOM_CYCLONEDX_JSON",
},
},
{
Name: "sbom", Type: "SBOM_CYCLONEDX_JSON",
Filename: "inline-sbom.json",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c"},
EmbeddedInline: true,
Value: "hello inline!",
Annotations: make(map[string]string),
Annotations: map[string]string{
"chainloop.material.cas.inline": "true",
"chainloop.material.name": "sbom",
"chainloop.material.type": "SBOM_CYCLONEDX_JSON",
},
},
{
Name: "stringvar", Type: "STRING",
Value: "helloworld",
Annotations: make(map[string]string),
Value: "helloworld",
Annotations: map[string]string{
"chainloop.material.name": "stringvar",
"chainloop.material.type": "STRING",
},
},
},
},
Expand Down
35 changes: 26 additions & 9 deletions pkg/attestation/renderer/chainloop/v02.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2026 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.
Expand All @@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -429,17 +430,11 @@ func (p *ProvenancePredicateV02) GetPolicyEvaluationStatus() *PolicyEvaluationSt
func normalizeMaterial(material *intoto.ResourceDescriptor) (*NormalizedMaterial, error) {
m := &NormalizedMaterial{}

// Set custom annotations
// Set annotations (both custom and chainloop.* prefixed)
m.Annotations = make(map[string]string)
mAnnotationsMap := material.Annotations.GetFields()
for k, v := range mAnnotationsMap {
// if the annotation key doesn't start with chainloop.
// we set it as a custom annotation
if strings.HasPrefix(k, v1.AnnotationPrefix) {
continue
}

m.Annotations[k] = v.GetStringValue()
m.Annotations[k] = structValueToString(v)
}

mType, ok := mAnnotationsMap[v1.AnnotationMaterialType]
Expand Down Expand Up @@ -530,3 +525,25 @@ func normalizeMaterial(material *intoto.ResourceDescriptor) (*NormalizedMaterial

return m, nil
}

// structValueToString converts a structpb.Value to its string representation.
// Some annotations (e.g. chainloop.material.cas) are stored as BoolValue,
// for which GetStringValue() returns "".
func structValueToString(v *structpb.Value) string {
switch v.GetKind().(type) {
case *structpb.Value_StringValue:
return v.GetStringValue()
case *structpb.Value_BoolValue:
return strconv.FormatBool(v.GetBoolValue())
case *structpb.Value_NumberValue:
return fmt.Sprintf("%g", v.GetNumberValue())
case *structpb.Value_StructValue, *structpb.Value_ListValue:
b, err := json.Marshal(v.AsInterface())
if err != nil {
return ""
}
return string(b)
default:
return ""
}
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
73 changes: 63 additions & 10 deletions pkg/attestation/renderer/chainloop/v02_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024-2025 The Chainloop Authors.
// Copyright 2024-2026 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.
Expand Down Expand Up @@ -113,8 +113,6 @@ func mapToStruct(t *testing.T, input map[string]interface{}) *structpb.Struct {
}

func TestNormalizeMaterial(t *testing.T) {
var emptyMap = make(map[string]string)

testCases := []struct {
name string
input *intoto.ResourceDescriptor
Expand Down Expand Up @@ -160,10 +158,13 @@ func TestNormalizeMaterial(t *testing.T) {
Content: []byte("bar"),
},
want: &NormalizedMaterial{
Name: "foo",
Type: "STRING",
Value: "bar",
Annotations: emptyMap,
Name: "foo",
Type: "STRING",
Value: "bar",
Annotations: map[string]string{
"chainloop.material.name": "foo",
"chainloop.material.type": "STRING",
},
},
},
{
Expand Down Expand Up @@ -195,7 +196,11 @@ func TestNormalizeMaterial(t *testing.T) {
Filename: "artifact.tgz",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"},
UploadedToCAS: true,
Annotations: emptyMap,
Annotations: map[string]string{
"chainloop.material.name": "foo",
"chainloop.material.type": "ARTIFACT",
"chainloop.material.cas": "true",
},
},
},
{
Expand All @@ -219,7 +224,13 @@ func TestNormalizeMaterial(t *testing.T) {
Filename: "artifact.tgz",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"},
UploadedToCAS: true,
Annotations: map[string]string{"foo": "bar", "bar": "baz"},
Annotations: map[string]string{
"foo": "bar",
"bar": "baz",
"chainloop.material.name": "foo",
"chainloop.material.type": "ARTIFACT",
"chainloop.material.cas": "true",
},
},
},
{
Expand All @@ -243,7 +254,11 @@ func TestNormalizeMaterial(t *testing.T) {
Value: "this is an inline material",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"},
EmbeddedInline: true,
Annotations: emptyMap,
Annotations: map[string]string{
"chainloop.material.name": "foo",
"chainloop.material.type": "ARTIFACT",
"chainloop.material.cas.inline": "true",
},
},
},
{
Expand Down Expand Up @@ -285,6 +300,44 @@ func TestNormalizeMaterial(t *testing.T) {
}
}

func TestStructValueToString(t *testing.T) {
testCases := []struct {
name string
input *structpb.Value
want string
}{
{name: "string value", input: structpb.NewStringValue("hello"), want: "hello"},
{name: "empty string", input: structpb.NewStringValue(""), want: ""},
{name: "bool true", input: structpb.NewBoolValue(true), want: "true"},
{name: "bool false", input: structpb.NewBoolValue(false), want: "false"},
{name: "number int", input: structpb.NewNumberValue(42), want: "42"},
{name: "number float", input: structpb.NewNumberValue(3.14), want: "3.14"},
{name: "null value", input: structpb.NewNullValue(), want: ""},
{name: "list value", input: mustListValue(t, []any{"a", "b"}), want: `["a","b"]`},
{name: "struct value", input: mustStructValue(t, map[string]any{"k": "v"}), want: `{"k":"v"}`},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, structValueToString(tc.input))
})
}
}

func mustListValue(t *testing.T, items []any) *structpb.Value {
t.Helper()
list, err := structpb.NewList(items)
require.NoError(t, err)
return structpb.NewListValue(list)
}

func mustStructValue(t *testing.T, fields map[string]any) *structpb.Value {
t.Helper()
s, err := structpb.NewStruct(fields)
require.NoError(t, err)
return structpb.NewStructValue(s)
}

func TestPolicyEvaluationsField(t *testing.T) {
raw, err := os.ReadFile("testdata/attestation-pe-snake.json")
require.NoError(t, err)
Expand Down
Loading