diff --git a/bake/hcl_test.go b/bake/hcl_test.go index ac507a9b..7ee8a259 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -608,7 +608,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) { target "app" { attest = [ { type = "provenance", mode = "max" }, - "type=sbom,disabled=true", + "type=sbom,disabled=true,generator=foo,\"ENV1=bar,baz\",ENV2=hello", ] cache-from = [ @@ -641,7 +641,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) - require.Equal(t, []string{"type=provenance,mode=max", "type=sbom,disabled=true"}, stringify(c.Targets[0].Attest)) + require.Equal(t, []string{"type=provenance,mode=max", "type=sbom,disabled=true,\"ENV1=bar,baz\",ENV2=hello,generator=foo"}, stringify(c.Targets[0].Attest)) require.Equal(t, []string{"type=local,dest=../out", "type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs)) require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom)) require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo)) diff --git a/util/buildflags/attests.go b/util/buildflags/attests.go index e0fe1e2f..54997307 100644 --- a/util/buildflags/attests.go +++ b/util/buildflags/attests.go @@ -148,9 +148,8 @@ func (a *Attest) UnmarshalText(text []byte) error { if !ok { return errors.Errorf("invalid value %s", field) } - key = strings.TrimSpace(strings.ToLower(key)) - switch key { + switch strings.TrimSpace(strings.ToLower(key)) { case "type": a.Type = value case "disabled": diff --git a/util/buildflags/attests_test.go b/util/buildflags/attests_test.go index c18545f2..6e2c942f 100644 --- a/util/buildflags/attests_test.go +++ b/util/buildflags/attests_test.go @@ -13,16 +13,21 @@ func TestAttests(t *testing.T) { attests := Attests{ {Type: "provenance", Attrs: map[string]string{"mode": "max"}}, {Type: "sbom", Disabled: true}, + {Type: "sbom", Attrs: map[string]string{ + "generator": "scanner", + "ENV1": `"foo,bar"`, + "Env2": "hello", + }}, } - expected := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]` + expected := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true},{"ENV1":"\"foo,bar\"","Env2":"hello","generator":"scanner","type":"sbom"}]` actual, err := json.Marshal(attests) require.NoError(t, err) require.JSONEq(t, expected, string(actual)) }) t.Run("UnmarshalJSON", func(t *testing.T) { - in := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]` + in := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true},{"ENV1":"\"foo,bar\"","Env2":"hello","generator":"scanner","type":"sbom"}]` var actual Attests err := json.Unmarshal([]byte(in), &actual) @@ -31,6 +36,11 @@ func TestAttests(t *testing.T) { expected := Attests{ {Type: "provenance", Attrs: map[string]string{"mode": "max"}}, {Type: "sbom", Disabled: true, Attrs: map[string]string{}}, + {Type: "sbom", Disabled: false, Attrs: map[string]string{ + "generator": "scanner", + "ENV1": `"foo,bar"`, + "Env2": "hello", + }}, } require.Equal(t, expected, actual) }) @@ -41,7 +51,14 @@ func TestAttests(t *testing.T) { "type": cty.StringVal("provenance"), "mode": cty.StringVal("max"), }), + cty.ObjectVal(map[string]cty.Value{ + "type": cty.StringVal("sbom"), + "generator": cty.StringVal("scan"), + "ENV1": cty.StringVal(`foo,bar`), + "Env2": cty.StringVal(`hello`), + }), cty.StringVal("type=sbom,disabled=true"), + cty.StringVal(`type=sbom,generator=scan,"FOO=bar,baz",Hello=World`), }) var actual Attests @@ -50,7 +67,17 @@ func TestAttests(t *testing.T) { expected := Attests{ {Type: "provenance", Attrs: map[string]string{"mode": "max"}}, + {Type: "sbom", Attrs: map[string]string{ + "generator": "scan", + "ENV1": "foo,bar", + "Env2": "hello", + }}, {Type: "sbom", Disabled: true, Attrs: map[string]string{}}, + {Type: "sbom", Attrs: map[string]string{ + "generator": "scan", + "FOO": "bar,baz", + "Hello": "World", + }}, } require.Equal(t, expected, actual) }) @@ -59,6 +86,11 @@ func TestAttests(t *testing.T) { attests := Attests{ {Type: "provenance", Attrs: map[string]string{"mode": "max"}}, {Type: "sbom", Disabled: true}, + {Type: "sbom", Attrs: map[string]string{ + "generator": "scan", + "ENV1": `"foo,bar"`, + "Env2": "hello", + }}, } actual := attests.ToCtyValue() @@ -71,6 +103,12 @@ func TestAttests(t *testing.T) { "type": cty.StringVal("sbom"), "disabled": cty.StringVal("true"), }), + cty.MapVal(map[string]cty.Value{ + "type": cty.StringVal("sbom"), + "generator": cty.StringVal("scan"), + "ENV1": cty.StringVal(`"foo,bar"`), + "Env2": cty.StringVal("hello"), + }), }) result := actual.Equals(expected) diff --git a/util/buildflags/export.go b/util/buildflags/export.go index ba364c9f..8460d3e2 100644 --- a/util/buildflags/export.go +++ b/util/buildflags/export.go @@ -1,6 +1,7 @@ package buildflags import ( + "encoding/csv" "encoding/json" "maps" "regexp" @@ -259,9 +260,18 @@ func (w *csvBuilder) Write(key, value string) { if w.sb.Len() > 0 { w.sb.WriteByte(',') } - w.sb.WriteString(key) - w.sb.WriteByte('=') - w.sb.WriteString(value) + + pair := key + "=" + value + if strings.ContainsRune(pair, ',') || strings.ContainsRune(pair, '"') { + var attr strings.Builder + writer := csv.NewWriter(&attr) + writer.Write([]string{pair}) + writer.Flush() + // Strips the extra newline added by the csv writer + pair = strings.TrimSpace(attr.String()) + } + + w.sb.WriteString(pair) } func (w *csvBuilder) WriteAttributes(attrs map[string]string) {