Skip to content

Commit 990496d

Browse files
authored
fix: stabilize materials output (#100)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent ad2e64f commit 990496d

File tree

7 files changed

+264
-16
lines changed

7 files changed

+264
-16
lines changed

app/cli/cmd/attestation_push.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func newAttestationPushCmd() *cobra.Command {
5656
return fmt.Errorf("getting executable information: %w", err)
5757
}
5858
a := action.NewAttestationPush(&action.AttestationPushOpts{
59-
ActionsOpts: actionOpts, KeyPath: pkPath, CLIversion: info.Version, CLIDigest: info.Digest,
59+
ActionsOpts: actionOpts, KeyPath: pkPath, CLIVersion: info.Version, CLIDigest: info.Digest,
6060
})
6161

6262
res, err := a.Run()

app/cli/internal/action/attestation_push.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828

2929
type AttestationPushOpts struct {
3030
*ActionsOpts
31-
KeyPath, CLIversion, CLIDigest string
31+
KeyPath, CLIVersion, CLIDigest string
3232
}
3333

3434
type AttestationPush struct {
@@ -42,7 +42,7 @@ func NewAttestationPush(cfg *AttestationPushOpts) *AttestationPush {
4242
ActionsOpts: cfg.ActionsOpts,
4343
c: crafter.NewCrafter(crafter.WithLogger(&cfg.Logger)),
4444
keyPath: cfg.KeyPath,
45-
cliVersion: cfg.CLIversion,
45+
cliVersion: cfg.CLIVersion,
4646
cliDigest: cfg.CLIDigest,
4747
}
4848
}
@@ -64,7 +64,7 @@ func (action *AttestationPush) Run() (interface{}, error) {
6464

6565
action.Logger.Debug().Msg("validation completed")
6666

67-
renderer, err := renderer.NewAttestationRenderer(action.c.CraftingState, action.keyPath, action.cliVersion, action.cliDigest, &action.Logger)
67+
renderer, err := renderer.NewAttestationRenderer(action.c.CraftingState, action.keyPath, action.cliVersion, action.cliDigest, renderer.WithLogger(action.Logger))
6868
if err != nil {
6969
return nil, err
7070
}

internal/attestation/renderer/chainloop.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"crypto/sha256"
2020
"encoding/json"
2121
"fmt"
22+
"sort"
2223
"strings"
2324
"time"
2425

@@ -95,20 +96,23 @@ type ChainloopMaintainer struct {
9596
}
9697

9798
type ChainloopRenderer struct {
98-
att *v1.Attestation
99-
schema *schemaapi.CraftingSchema
99+
att *v1.Attestation
100+
builder *builderInfo
101+
}
102+
103+
type builderInfo struct {
100104
version, digest string
101105
}
102106

103-
func newChainloopRenderer(att *v1.Attestation, schema *schemaapi.CraftingSchema, version, digest string) *ChainloopRenderer {
104-
return &ChainloopRenderer{att, schema, version, digest}
107+
func newChainloopRenderer(att *v1.Attestation, builderVersion, builderDigest string) *ChainloopRenderer {
108+
return &ChainloopRenderer{att, &builderInfo{builderVersion, builderDigest}}
105109
}
106110

107111
func (r *ChainloopRenderer) Predicate() (interface{}, error) {
108112
return ChainloopProvenancePredicateV1{
109113
Materials: outputChainloopMaterials(r.att, false),
110114
BuildType: chainloopBuildType,
111-
Builder: &slsacommon.ProvenanceBuilder{ID: fmt.Sprintf(builderIDFmt, r.version, r.digest)},
115+
Builder: &slsacommon.ProvenanceBuilder{ID: fmt.Sprintf(builderIDFmt, r.builder.version, r.builder.digest)},
112116
Metadata: getChainloopMeta(r.att),
113117
Env: r.att.EnvVars,
114118
RunnerType: r.att.GetRunnerType().String(),
@@ -167,8 +171,19 @@ func (r *ChainloopRenderer) Header() (*in_toto.StatementHeader, error) {
167171
}
168172

169173
func outputChainloopMaterials(att *v1.Attestation, onlyOutput bool) []*ChainloopProvenanceMaterial {
174+
// Sort material keys to stabilize output
175+
keys := make([]string, 0, len(att.GetMaterials()))
176+
for k := range att.GetMaterials() {
177+
keys = append(keys, k)
178+
}
179+
180+
sort.Strings(keys)
181+
170182
res := []*ChainloopProvenanceMaterial{}
171-
for mdefName, mdef := range att.GetMaterials() {
183+
materials := att.GetMaterials()
184+
for _, mdefName := range keys {
185+
mdef := materials[mdefName]
186+
172187
var value, digest string
173188
artifactType := mdef.MaterialType
174189
var isOutput bool

internal/attestation/renderer/chainloop_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,67 @@ import (
2121
"os"
2222
"testing"
2323

24+
api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1"
25+
"github.com/in-toto/in-toto-golang/in_toto"
2426
"github.com/secure-systems-lab/go-securesystemslib/dsse"
2527
"github.com/stretchr/testify/assert"
2628
"github.com/stretchr/testify/require"
29+
"google.golang.org/protobuf/encoding/protojson"
2730
)
2831

32+
func TestRender(t *testing.T) {
33+
testCases := []struct {
34+
name string
35+
sourcePath string
36+
outputPath string
37+
}{
38+
{
39+
name: "render v0.1",
40+
sourcePath: "testdata/attestation.source.json",
41+
outputPath: "testdata/attestation.output.v0.1.json",
42+
},
43+
}
44+
45+
for _, tc := range testCases {
46+
t.Run(tc.name, func(t *testing.T) {
47+
// Load expected resulting output
48+
wantRaw, err := os.ReadFile(tc.outputPath)
49+
require.NoError(t, err)
50+
51+
var want *in_toto.Statement
52+
err = json.Unmarshal(wantRaw, &want)
53+
require.NoError(t, err)
54+
55+
// Initialize renderer
56+
state := &api.CraftingState{}
57+
stateRaw, err := os.ReadFile(tc.sourcePath)
58+
require.NoError(t, err)
59+
60+
err = protojson.Unmarshal(stateRaw, state)
61+
require.NoError(t, err)
62+
63+
renderer, err := NewAttestationRenderer(state, "", "dev", "sha256:59e14f1a9de709cdd0e91c36b33e54fcca95f7dba1dc7169a7f81986e02108e5")
64+
require.NoError(t, err)
65+
66+
// Compare header
67+
gotHeader, err := renderer.renderer.Header()
68+
assert.NoError(t, err)
69+
assert.Equal(t, want.Type, gotHeader.Type)
70+
assert.Equal(t, want.Subject, gotHeader.Subject)
71+
assert.Equal(t, want.PredicateType, gotHeader.PredicateType)
72+
73+
// Compare predicate
74+
gotPredicateI, err := renderer.renderer.Predicate()
75+
assert.NoError(t, err)
76+
gotPredicate := gotPredicateI.(ChainloopProvenancePredicateV1)
77+
wantPredicate, err := extractPredicateV1(want)
78+
wantPredicate.Metadata.FinishedAt = gotPredicate.Metadata.FinishedAt
79+
assert.NoError(t, err)
80+
assert.EqualValues(t, wantPredicate, &gotPredicate)
81+
})
82+
}
83+
}
84+
2985
func TestExtractPredicate(t *testing.T) {
3086
testCases := []struct {
3187
name string

internal/attestation/renderer/renderer.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import (
3535
)
3636

3737
type AttestationRenderer struct {
38-
logger *zerolog.Logger
38+
logger zerolog.Logger
3939
signingKeyPath string
4040
att *v1.Attestation
4141
renderer r
@@ -46,17 +46,31 @@ type r interface {
4646
Predicate() (interface{}, error)
4747
}
4848

49-
func NewAttestationRenderer(state *v1.CraftingState, keyPath, builderVersion, builderDigest string, logger *zerolog.Logger) (*AttestationRenderer, error) {
49+
type Opt func(*AttestationRenderer)
50+
51+
func WithLogger(logger zerolog.Logger) Opt {
52+
return func(ar *AttestationRenderer) {
53+
ar.logger = logger
54+
}
55+
}
56+
57+
func NewAttestationRenderer(state *v1.CraftingState, keyPath, builderVersion, builderDigest string, opts ...Opt) (*AttestationRenderer, error) {
5058
if state.GetAttestation() == nil {
5159
return nil, errors.New("attestation not initialized")
5260
}
5361

54-
return &AttestationRenderer{
55-
logger: logger,
62+
r := &AttestationRenderer{
63+
logger: zerolog.Nop(),
5664
signingKeyPath: keyPath,
5765
att: state.GetAttestation(),
58-
renderer: newChainloopRenderer(state.GetAttestation(), state.GetInputSchema(), builderVersion, builderDigest),
59-
}, nil
66+
renderer: newChainloopRenderer(state.GetAttestation(), builderVersion, builderDigest),
67+
}
68+
69+
for _, opt := range opts {
70+
opt(r)
71+
}
72+
73+
return r, nil
6074
}
6175

6276
// Attestation (dsee envelope) -> { message: { Statement(in-toto): [subject, predicate] }, signature: "sig" }.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"_type": "https://in-toto.io/Statement/v0.1",
3+
"predicateType": "chainloop.dev/attestation/v0.1",
4+
"subject": [
5+
{
6+
"name": "chainloop.dev/workflow/foo",
7+
"digest": {
8+
"sha256": "6cd649c6105e12a235510a585371eb69c8c9ee797d8dc80d30695828ca116b00"
9+
}
10+
},
11+
{
12+
"name": "index.docker.io/bitnami/nginx",
13+
"digest": {
14+
"sha256": "580ac09da7771920dfd0c214964e7bfe4c27903bcbe075769a4044a67c9a390a"
15+
}
16+
}
17+
],
18+
"predicate": {
19+
"metadata": {
20+
"name": "foo",
21+
"project": "bar",
22+
"team": "",
23+
"initializedAt": "2023-05-03T17:22:12.743426076Z",
24+
"finishedAt": "2023-05-03T19:27:51.352850152+02:00",
25+
"workflowRunID": "",
26+
"workflowID": "54ea7c5c-7592-48ac-9a9f-084b72447184"
27+
},
28+
"materials": [
29+
{
30+
"name": "build-ref",
31+
"type": "STRING",
32+
"material": {
33+
"stringVal": "a-string"
34+
}
35+
},
36+
{
37+
"name": "rootfs",
38+
"type": "ARTIFACT",
39+
"material": {
40+
"slsa": {
41+
"uri": "Makefile",
42+
"digest": {
43+
"sha256": "cfc7d8e24d21ade921d720228ad1693de59dab45ff679606940be75b7bf660dc"
44+
}
45+
}
46+
}
47+
},
48+
{
49+
"name": "skynet-control-plane",
50+
"type": "CONTAINER_IMAGE",
51+
"material": {
52+
"slsa": {
53+
"uri": "index.docker.io/bitnami/nginx",
54+
"digest": {
55+
"sha256": "580ac09da7771920dfd0c214964e7bfe4c27903bcbe075769a4044a67c9a390a"
56+
}
57+
}
58+
}
59+
},
60+
{
61+
"name": "skynet-sbom",
62+
"type": "SBOM_CYCLONEDX_JSON",
63+
"material": {
64+
"slsa": {
65+
"uri": "sbom.cyclonedx.json",
66+
"digest": {
67+
"sha256": "16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c"
68+
}
69+
}
70+
}
71+
}
72+
],
73+
"builder": {
74+
"id": "chainloop.dev/cli/dev@sha256:59e14f1a9de709cdd0e91c36b33e54fcca95f7dba1dc7169a7f81986e02108e5"
75+
},
76+
"buildType": "chainloop.dev/workflowrun/v0.1",
77+
"runnerType": "GITHUB_ACTION"
78+
}
79+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"inputSchema": {
3+
"schemaVersion": "v1",
4+
"materials": [
5+
{
6+
"type": "CONTAINER_IMAGE",
7+
"name": "skynet-control-plane",
8+
"output": true
9+
},
10+
{
11+
"type": "ARTIFACT",
12+
"name": "rootfs"
13+
},
14+
{
15+
"type": "ARTIFACT",
16+
"name": "dockerfile",
17+
"optional": true
18+
},
19+
{
20+
"type": "STRING",
21+
"name": "build-ref"
22+
},
23+
{
24+
"type": "SBOM_CYCLONEDX_JSON",
25+
"name": "skynet-sbom"
26+
}
27+
],
28+
"envAllowList": [
29+
"CUSTOM_VAR"
30+
],
31+
"runner": {
32+
"type": "GITHUB_ACTION"
33+
}
34+
},
35+
"attestation": {
36+
"initializedAt": "2023-05-03T17:22:12.743426076Z",
37+
"workflow": {
38+
"name": "foo",
39+
"project": "bar",
40+
"workflowId": "54ea7c5c-7592-48ac-9a9f-084b72447184",
41+
"schemaRevision": "1"
42+
},
43+
"materials": {
44+
"build-ref": {
45+
"string": {
46+
"id": "build-ref",
47+
"value": "a-string"
48+
},
49+
"addedAt": "2023-05-03T17:23:27.113091137Z",
50+
"materialType": "STRING"
51+
},
52+
"rootfs": {
53+
"artifact": {
54+
"id": "rootfs",
55+
"name": "Makefile",
56+
"digest": "sha256:cfc7d8e24d21ade921d720228ad1693de59dab45ff679606940be75b7bf660dc"
57+
},
58+
"addedAt": "2023-05-03T17:23:13.548426342Z",
59+
"materialType": "ARTIFACT"
60+
},
61+
"skynet-control-plane": {
62+
"containerImage": {
63+
"id": "skynet-control-plane",
64+
"name": "index.docker.io/bitnami/nginx",
65+
"digest": "sha256:580ac09da7771920dfd0c214964e7bfe4c27903bcbe075769a4044a67c9a390a",
66+
"isSubject": true
67+
},
68+
"addedAt": "2023-05-03T17:22:49.616972571Z",
69+
"materialType": "CONTAINER_IMAGE"
70+
},
71+
"skynet-sbom": {
72+
"artifact": {
73+
"id": "skynet-sbom",
74+
"name": "sbom.cyclonedx.json",
75+
"digest": "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c"
76+
},
77+
"addedAt": "2023-05-03T17:24:31.956266292Z",
78+
"materialType": "SBOM_CYCLONEDX_JSON"
79+
}
80+
},
81+
"runnerType": "GITHUB_ACTION"
82+
},
83+
"dryRun": true
84+
}

0 commit comments

Comments
 (0)