Skip to content

Commit e0edc0c

Browse files
authored
feat: support for GCB v0.3 verification (#248)
* update * update * update * update
1 parent ff0ced4 commit e0edc0c

9 files changed

+720
-8
lines changed

errors/errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ var (
66
ErrorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
77
ErrorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
88
ErrorMismatchBuilderID = errors.New("builderID does not match provenance")
9+
ErrorInvalidBuilderID = errors.New("builderID is invalid")
910
ErrorMismatchSource = errors.New("source used to generate the binary does not match provenance")
1011
ErrorMismatchWorkflowInputs = errors.New("workflow input does not match")
1112
ErrorMalformedURI = errors.New("URI is malformed")
1213
ErrorMismatchTag = errors.New("tag used to generate the binary does not match provenance")
14+
ErrorInvalidRecipe = errors.New("the recipe is invalid")
1315
ErrorMismatchVersionedTag = errors.New("tag used to generate the binary does not match provenance")
1416
ErrorInvalidSemver = errors.New("invalid semantic version")
1517
ErrorRekorSearch = errors.New("error searching rekor entries")

verifiers/internal/gcb/provenance.go

+54-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import (
2121
"github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb/keys"
2222
)
2323

24-
var GCBBuilderIDs = []string{"https://cloudbuild.googleapis.com/[email protected]"}
24+
var GCBBuilderIDs = []string{
25+
"https://cloudbuild.googleapis.com/[email protected]",
26+
"https://cloudbuild.googleapis.com/[email protected]",
27+
}
2528

2629
type v01IntotoStatement struct {
2730
intoto.StatementHeader
@@ -215,7 +218,53 @@ func isValidBuilderID(id string) error {
215218
return nil
216219
}
217220
}
218-
return serrors.ErrorMismatchBuilderID
221+
return serrors.ErrorInvalidBuilderID
222+
}
223+
224+
func getBuilderVersion(builderID string) (string, error) {
225+
parts := strings.Split(builderID, "@")
226+
if len(parts) != 2 {
227+
return "", fmt.Errorf("%w: '%s'", serrors.ErrorInvalidFormat, parts)
228+
}
229+
return parts[1], nil
230+
}
231+
232+
func validateRecipeType(builderID, recipeType string) error {
233+
v, err := getBuilderVersion(builderID)
234+
if err != nil {
235+
return err
236+
}
237+
switch v {
238+
case "v0.2":
239+
// In this version, the recipe type should be the same as
240+
// the builder ID.
241+
if builderID == recipeType {
242+
return nil
243+
}
244+
err = fmt.Errorf("%w: expected '%s', got '%s'",
245+
serrors.ErrorInvalidRecipe, builderID, recipeType)
246+
247+
case "v0.3":
248+
// In this version, two recipe types are allowed, depending how the
249+
// build was made. We don't verify the version of the recipes,
250+
// because it's not super important and would add complexity.
251+
recipes := []string{
252+
"https://cloudbuild.googleapis.com/CloudBuildYaml@",
253+
"https://cloudbuild.googleapis.com/CloudBuildSteps@",
254+
}
255+
for _, r := range recipes {
256+
if strings.HasPrefix(recipeType, r) {
257+
return nil
258+
}
259+
}
260+
err = fmt.Errorf("%w: expected on of '%s', got '%s'",
261+
serrors.ErrorInvalidRecipe, strings.Join(recipes, ","), recipeType)
262+
default:
263+
err = fmt.Errorf("%w: version '%s'",
264+
serrors.ErrorInvalidBuilderID, v)
265+
}
266+
267+
return err
219268
}
220269

221270
// VerifyBuilder verifies the builder in the DSSE payload:
@@ -243,10 +292,9 @@ func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (string,
243292
}
244293
}
245294

246-
// Valiate that the recipe type is consistent.
247-
if predicateBuilderID != statement.Predicate.Recipe.Type {
248-
return "", fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID,
249-
predicateBuilderID, statement.Predicate.Recipe.Type)
295+
// Valiate the recipe type.
296+
if err := validateRecipeType(predicateBuilderID, statement.Predicate.Recipe.Type); err != nil {
297+
return "", err
250298
}
251299

252300
// Validate the recipe argument type.

verifiers/internal/gcb/provenance_test.go

+100-2
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,43 @@ func Test_VerifyBuilder(t *testing.T) {
120120
expected: serrors.ErrorMismatchBuilderID,
121121
},
122122
{
123-
name: "mismatch recipe.type",
123+
name: "v0.2 mismatch recipe.type",
124124
path: "./testdata/gcloud-container-invalid-recipe.type.json",
125-
expected: serrors.ErrorMismatchBuilderID,
125+
expected: serrors.ErrorInvalidRecipe,
126+
},
127+
{
128+
name: "v0.1 invalid builder",
129+
path: "./testdata/gcloud-container-invalid-builderv01.json",
130+
builderID: "http://cloudbuild.googleapis.com/[email protected]",
131+
expected: serrors.ErrorInvalidBuilderID,
132+
},
133+
{
134+
name: "invalid v0.2 recipe type CloudBuildSteps",
135+
path: "./testdata/gcloud-container-invalid-recipetypestepsv02.json",
136+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
137+
expected: serrors.ErrorInvalidRecipe,
138+
},
139+
{
140+
name: "invalid v0.2 recipe type CloudBuildYaml",
141+
path: "./testdata/gcloud-container-invalid-recipetypecloudv02.json",
142+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
143+
expected: serrors.ErrorInvalidRecipe,
144+
},
145+
{
146+
name: "valid v0.3 recipe type CloudBuildSteps",
147+
path: "./testdata/gcloud-container-invalid-recipetypestepsv03.json",
148+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
149+
},
150+
{
151+
name: "valid v0.3 recipe type CloudBuildYaml",
152+
path: "./testdata/gcloud-container-invalid-recipetypecloudv03.json",
153+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
154+
},
155+
{
156+
name: "invalid v0.3 recipe type random",
157+
path: "./testdata/gcloud-container-invalid-recipetyperandv03.json",
158+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
159+
expected: serrors.ErrorInvalidRecipe,
126160
},
127161
}
128162
for _, tt := range tests {
@@ -164,6 +198,70 @@ func Test_VerifyBuilder(t *testing.T) {
164198
}
165199
}
166200

201+
func Test_validateRecipeType(t *testing.T) {
202+
t.Parallel()
203+
tests := []struct {
204+
name string
205+
builderID string
206+
recipeType string
207+
expected error
208+
}{
209+
// v0.2 builder.
210+
{
211+
name: "valid v0.2 recipe type",
212+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
213+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
214+
},
215+
{
216+
name: "invalid v0.2 recipe type CloudBuildYaml",
217+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
218+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
219+
expected: serrors.ErrorInvalidRecipe,
220+
},
221+
{
222+
name: "invalid v0.2 recipe type CloudBuildSteps",
223+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
224+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
225+
expected: serrors.ErrorInvalidRecipe,
226+
},
227+
// v0.3 builder.
228+
{
229+
name: "valid v0.3 recipe type CloudBuildYaml",
230+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
231+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
232+
},
233+
{
234+
name: "valid v0.3 recipe type CloudBuildSteps",
235+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
236+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
237+
},
238+
{
239+
name: "invalid v0.3 recipe type GoogleHostedWorker",
240+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
241+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
242+
expected: serrors.ErrorInvalidRecipe,
243+
},
244+
// No version.
245+
{
246+
name: "invalid builder version",
247+
builderID: "https://cloudbuild.googleapis.com/[email protected]",
248+
recipeType: "https://cloudbuild.googleapis.com/[email protected]",
249+
expected: serrors.ErrorInvalidBuilderID,
250+
},
251+
}
252+
for _, tt := range tests {
253+
tt := tt // Re-initializing variable so it is not changed while executing the closure below
254+
t.Run(tt.name, func(t *testing.T) {
255+
t.Parallel()
256+
257+
err := validateRecipeType(tt.builderID, tt.recipeType)
258+
if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) {
259+
t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
260+
}
261+
})
262+
}
263+
}
264+
167265
func Test_VerifySourceURI(t *testing.T) {
168266
t.Parallel()
169267
tests := []struct {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"image_summary": {
3+
"digest": "sha256:1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd",
4+
"fully_qualified_digest": "us-west2-docker.pkg.dev/gosst-scare-sandbox/quickstart-docker-repo/quickstart-image@sha256:1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd",
5+
"registry": "us-west2-docker.pkg.dev",
6+
"repository": "quickstart-docker-repo"
7+
},
8+
"provenance_summary": {
9+
"provenance": [
10+
{
11+
"build": {
12+
"intotoStatement": {
13+
"_type": "https://in-toto.io/Statement/v0.1",
14+
"predicateType": "https://slsa.dev/provenance/v0.1",
15+
"slsaProvenance": {
16+
"builder": {
17+
"id": "https://cloudbuild.googleapis.com/[email protected]"
18+
},
19+
"materials": [
20+
{
21+
"uri": "https://github.com/laurentsimon/gcb-tests/commit/fbbb98765e85ad464302dc5977968104d36e455e"
22+
}
23+
],
24+
"metadata": {
25+
"buildFinishedOn": "2022-08-15T22:43:34.366498Z",
26+
"buildInvocationId": "b6e052a7-5aa4-41bf-a56b-9bc4e4f3058b",
27+
"buildStartedOn": "2022-08-15T22:43:18.700638187Z"
28+
},
29+
"recipe": {
30+
"arguments": {
31+
"@type": "type.googleapis.com/google.devtools.cloudbuild.v1.Build",
32+
"id": "b6e052a7-5aa4-41bf-a56b-9bc4e4f3058b",
33+
"options": {
34+
"dynamicSubstitutions": true,
35+
"logging": "LEGACY",
36+
"pool": {},
37+
"substitutionOption": "ALLOW_LOOSE"
38+
},
39+
"sourceProvenance": {},
40+
"steps": [
41+
{
42+
"args": [
43+
"build",
44+
"-t",
45+
"us-west2-docker.pkg.dev/gosst-scare-sandbox/quickstart-docker-repo/quickstart-image:v14",
46+
"."
47+
],
48+
"name": "gcr.io/cloud-builders/docker",
49+
"pullTiming": {
50+
"endTime": "2022-08-15T22:43:21.662016533Z",
51+
"startTime": "2022-08-15T22:43:21.657262492Z"
52+
},
53+
"status": "SUCCESS",
54+
"timing": {
55+
"endTime": "2022-08-15T22:43:27.056377441Z",
56+
"startTime": "2022-08-15T22:43:21.657262492Z"
57+
}
58+
}
59+
]
60+
},
61+
"entryPoint": "cloudbuild.yaml",
62+
"type": "https://cloudbuild.googleapis.com/[email protected]"
63+
}
64+
},
65+
"subject": [
66+
{
67+
"digest": {
68+
"sha256": "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd"
69+
},
70+
"name": "https://us-west2-docker.pkg.dev/gosst-scare-sandbox/quickstart-docker-repo/quickstart-image:v14"
71+
}
72+
]
73+
}
74+
},
75+
"createTime": "2022-08-15T22:43:35.649016Z",
76+
"envelope": {
77+
"payload": "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGUiOiB7CiAgICAiYnVpbGRlciI6IHsKICAgICAgImlkIjogImh0dHBzOi8vY2xvdWRidWlsZC5nb29nbGVhcGlzLmNvbS9Hb29nbGVIb3N0ZWRXb3JrZXJAdjAuMSIKICAgIH0sCiAgICAibWF0ZXJpYWxzIjogWwogICAgICB7CiAgICAgICAgInVyaSI6ICJodHRwczovL2dpdGh1Yi5jb20vbGF1cmVudHNpbW9uL2djYi10ZXN0cy9jb21taXQvZmJiYjk4NzY1ZTg1YWQ0NjQzMDJkYzU5Nzc5NjgxMDRkMzZlNDU1ZSIKICAgICAgfQogICAgXSwKICAgICJtZXRhZGF0YSI6IHsKICAgICAgImJ1aWxkRmluaXNoZWRPbiI6ICIyMDIyLTA4LTE1VDIyOjQzOjM0LjM2NjQ5OFoiLAogICAgICAiYnVpbGRJbnZvY2F0aW9uSWQiOiAiYjZlMDUyYTctNWFhNC00MWJmLWE1NmItOWJjNGU0ZjMwNThiIiwKICAgICAgImJ1aWxkU3RhcnRlZE9uIjogIjIwMjItMDgtMTVUMjI6NDM6MTguNzAwNjM4MTg3WiIKICAgIH0sCiAgICAicmVjaXBlIjogewogICAgICAiYXJndW1lbnRzIjogewogICAgICAgICJAdHlwZSI6ICJ0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5kZXZ0b29scy5jbG91ZGJ1aWxkLnYxLkJ1aWxkIiwKICAgICAgICAiaWQiOiAiYjZlMDUyYTctNWFhNC00MWJmLWE1NmItOWJjNGU0ZjMwNThiIiwKICAgICAgICAib3B0aW9ucyI6IHsKICAgICAgICAgICJkeW5hbWljU3Vic3RpdHV0aW9ucyI6IHRydWUsCiAgICAgICAgICAibG9nZ2luZyI6ICJMRUdBQ1kiLAogICAgICAgICAgInBvb2wiOiB7fSwKICAgICAgICAgICJzdWJzdGl0dXRpb25PcHRpb24iOiAiQUxMT1dfTE9PU0UiCiAgICAgICAgfSwKICAgICAgICAic291cmNlUHJvdmVuYW5jZSI6IHt9LAogICAgICAgICJzdGVwcyI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImFyZ3MiOiBbCiAgICAgICAgICAgICAgImJ1aWxkIiwKICAgICAgICAgICAgICAiLXQiLAogICAgICAgICAgICAgICJ1cy13ZXN0Mi1kb2NrZXIucGtnLmRldi9nb3NzdC1zY2FyZS1zYW5kYm94L3F1aWNrc3RhcnQtZG9ja2VyLXJlcG8vcXVpY2tzdGFydC1pbWFnZTp2MTQiLAogICAgICAgICAgICAgICIuIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJnY3IuaW8vY2xvdWQtYnVpbGRlcnMvZG9ja2VyIiwKICAgICAgICAgICAgInB1bGxUaW1pbmciOiB7CiAgICAgICAgICAgICAgImVuZFRpbWUiOiAiMjAyMi0wOC0xNVQyMjo0MzoyMS42NjIwMTY1MzNaIiwKICAgICAgICAgICAgICAic3RhcnRUaW1lIjogIjIwMjItMDgtMTVUMjI6NDM6MjEuNjU3MjYyNDkyWiIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInN0YXR1cyI6ICJTVUNDRVNTIiwKICAgICAgICAgICAgInRpbWluZyI6IHsKICAgICAgICAgICAgICAiZW5kVGltZSI6ICIyMDIyLTA4LTE1VDIyOjQzOjI3LjA1NjM3NzQ0MVoiLAogICAgICAgICAgICAgICJzdGFydFRpbWUiOiAiMjAyMi0wOC0xNVQyMjo0MzoyMS42NTcyNjI0OTJaIgogICAgICAgICAgICB9CiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAiZW50cnlQb2ludCI6ICJjbG91ZGJ1aWxkLnlhbWwiLAogICAgICAidHlwZSI6ICJodHRwczovL2Nsb3VkYnVpbGQuZ29vZ2xlYXBpcy5jb20vR29vZ2xlSG9zdGVkV29ya2VyQHYwLjIiCiAgICB9CiAgfSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMSIsCiAgInNsc2FQcm92ZW5hbmNlIjogewogICAgImJ1aWxkZXIiOiB7CiAgICAgICJpZCI6ICJodHRwczovL2Nsb3VkYnVpbGQuZ29vZ2xlYXBpcy5jb20vR29vZ2xlSG9zdGVkV29ya2VyQHYwLjIiCiAgICB9LAogICAgIm1hdGVyaWFscyI6IFsKICAgICAgewogICAgICAgICJ1cmkiOiAiaHR0cHM6Ly9naXRodWIuY29tL2xhdXJlbnRzaW1vbi9nY2ItdGVzdHMvY29tbWl0L2ZiYmI5ODc2NWU4NWFkNDY0MzAyZGM1OTc3OTY4MTA0ZDM2ZTQ1NWUiCiAgICAgIH0KICAgIF0sCiAgICAibWV0YWRhdGEiOiB7CiAgICAgICJidWlsZEZpbmlzaGVkT24iOiAiMjAyMi0wOC0xNVQyMjo0MzozNC4zNjY0OThaIiwKICAgICAgImJ1aWxkSW52b2NhdGlvbklkIjogImI2ZTA1MmE3LTVhYTQtNDFiZi1hNTZiLTliYzRlNGYzMDU4YiIsCiAgICAgICJidWlsZFN0YXJ0ZWRPbiI6ICIyMDIyLTA4LTE1VDIyOjQzOjE4LjcwMDYzODE4N1oiCiAgICB9LAogICAgInJlY2lwZSI6IHsKICAgICAgImFyZ3VtZW50cyI6IHsKICAgICAgICAiQHR5cGUiOiAidHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuZGV2dG9vbHMuY2xvdWRidWlsZC52MS5CdWlsZCIsCiAgICAgICAgImlkIjogImI2ZTA1MmE3LTVhYTQtNDFiZi1hNTZiLTliYzRlNGYzMDU4YiIsCiAgICAgICAgIm9wdGlvbnMiOiB7CiAgICAgICAgICAiZHluYW1pY1N1YnN0aXR1dGlvbnMiOiB0cnVlLAogICAgICAgICAgImxvZ2dpbmciOiAiTEVHQUNZIiwKICAgICAgICAgICJwb29sIjoge30sCiAgICAgICAgICAic3Vic3RpdHV0aW9uT3B0aW9uIjogIkFMTE9XX0xPT1NFIgogICAgICAgIH0sCiAgICAgICAgInNvdXJjZVByb3ZlbmFuY2UiOiB7fSwKICAgICAgICAic3RlcHMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJhcmdzIjogWwogICAgICAgICAgICAgICJidWlsZCIsCiAgICAgICAgICAgICAgIi10IiwKICAgICAgICAgICAgICAidXMtd2VzdDItZG9ja2VyLnBrZy5kZXYvZ29zc3Qtc2NhcmUtc2FuZGJveC9xdWlja3N0YXJ0LWRvY2tlci1yZXBvL3F1aWNrc3RhcnQtaW1hZ2U6djE0IiwKICAgICAgICAgICAgICAiLiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiZ2NyLmlvL2Nsb3VkLWJ1aWxkZXJzL2RvY2tlciIsCiAgICAgICAgICAgICJwdWxsVGltaW5nIjogewogICAgICAgICAgICAgICJlbmRUaW1lIjogIjIwMjItMDgtMTVUMjI6NDM6MjEuNjYyMDE2NTMzWiIsCiAgICAgICAgICAgICAgInN0YXJ0VGltZSI6ICIyMDIyLTA4LTE1VDIyOjQzOjIxLjY1NzI2MjQ5MloiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJzdGF0dXMiOiAiU1VDQ0VTUyIsCiAgICAgICAgICAgICJ0aW1pbmciOiB7CiAgICAgICAgICAgICAgImVuZFRpbWUiOiAiMjAyMi0wOC0xNVQyMjo0MzoyNy4wNTYzNzc0NDFaIiwKICAgICAgICAgICAgICAic3RhcnRUaW1lIjogIjIwMjItMDgtMTVUMjI6NDM6MjEuNjU3MjYyNDkyWiIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfSwKICAgICAgImVudHJ5UG9pbnQiOiAiY2xvdWRidWlsZC55YW1sIiwKICAgICAgInR5cGUiOiAiaHR0cHM6Ly9jbG91ZGJ1aWxkLmdvb2dsZWFwaXMuY29tL0dvb2dsZUhvc3RlZFdvcmtlckB2MC4yIgogICAgfQogIH0sCiAgInN1YmplY3QiOiBbCiAgICB7CiAgICAgICJkaWdlc3QiOiB7CiAgICAgICAgInNoYTI1NiI6ICIxYTAzM2IwMDJmODllZDJiOGVhNzMzMTYyNDk3ZmI3MGYxYTQwNDlhN2Y4NjAyZDZhMzM2ODJiNGFkOTkyMWZkIgogICAgICB9LAogICAgICAibmFtZSI6ICJodHRwczovL3VzLXdlc3QyLWRvY2tlci5wa2cuZGV2L2dvc3N0LXNjYXJlLXNhbmRib3gvcXVpY2tzdGFydC1kb2NrZXItcmVwby9xdWlja3N0YXJ0LWltYWdlOnYxNCIKICAgIH0KICBdCn0=",
78+
"payloadType": "application/vnd.in-toto+json",
79+
"signatures": [
80+
{
81+
"keyid": "projects/verified-builder/locations/global/keyRings/attestor/cryptoKeys/builtByGCB/cryptoKeyVersions/1",
82+
"sig": "MEYCIQD-0xUsdkYnsmKnQL_ndEvXknLfn82zsG-hGyYUd4aYsAIhAP4KSCxN2VPNc-dvfrQIGduMUNmAiHxLttdezqdrSf3F"
83+
}
84+
]
85+
},
86+
"kind": "BUILD",
87+
"name": "projects/gosst-scare-sandbox/occurrences/8ce06798-f94d-4772-a224-04e473163790",
88+
"noteName": "projects/verified-builder/notes/intoto_b6e052a7-5aa4-41bf-a56b-9bc4e4f3058b",
89+
"resourceUri": "https://us-west2-docker.pkg.dev/gosst-scare-sandbox/quickstart-docker-repo/quickstart-image@sha256:1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd",
90+
"updateTime": "2022-08-15T22:43:35.649016Z"
91+
}
92+
]
93+
}
94+
}

0 commit comments

Comments
 (0)