From a32b8d7fc62db177e08f30b472b0828fca0db6fc Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 25 Jan 2024 09:00:35 -0500 Subject: [PATCH] Use the json schema as input for templating (#2542) * use the json schema as input for templating Signed-off-by: Alex Goodman * fix cli tests Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- README.md | 17 ++++++++- cmd/syft/internal/options/format_template.go | 2 + syft/format/encoders.go | 4 +- syft/format/template/encoder.go | 27 +++++++++++++- syft/format/template/encoder_test.go | 37 ++++++++++++++++++- .../test-fixtures/csv-hasField.template | 4 +- .../template/test-fixtures/csv.template | 4 +- .../legacy/csv-hasField.template | 4 ++ .../test-fixtures/legacy/csv.template | 4 ++ ...tFormatWithOptionAndHasField_Legacy.golden | 3 ++ .../TestFormatWithOption_Legacy.golden | 3 ++ test/cli/test-fixtures/csv.template | 4 +- 12 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 syft/format/template/test-fixtures/legacy/csv-hasField.template create mode 100644 syft/format/template/test-fixtures/legacy/csv.template create mode 100644 syft/format/template/test-fixtures/snapshot/TestFormatWithOptionAndHasField_Legacy.golden create mode 100644 syft/format/template/test-fixtures/snapshot/TestFormatWithOption_Legacy.golden diff --git a/README.md b/README.md index 4692f3e49..bd6ad280e 100644 --- a/README.md +++ b/README.md @@ -555,6 +555,7 @@ select-catalogers: [] format: # default value for all formats that support the "pretty" option (default is unset) + # SYFT_FORMAT_PRETTY env var pretty: # all syft-json format options @@ -562,6 +563,7 @@ format: # include space indention and newlines (inherits default value from 'format.pretty' or 'false' if parent is unset) # note: inherits default value from 'format.pretty' or 'false' if parent is unset + # SYFT_FORMAT_JSON_PRETTY env var pretty: false # transform any syft-json output to conform to an approximation of the v11.0.1 schema. This includes: @@ -571,21 +573,30 @@ format: # that output might not strictly be json schema v11 compliant, however, for consumers that require time to port # over to the final syft 1.0 json output this option can be used to ease the transition. # - # Note: long term support for this option is not guaranteed. + # Note: long term support for this option is not guaranteed (it may change or break at any time). + # SYFT_FORMAT_JSON_LEGACY env var legacy: false # all template format options template: # path to the template file to use when rendering the output with the `template` output format. # Note that all template paths are based on the current syft-json schema. - # SYFT_TEMPLATE_PATH env var / -t flag + # SYFT_FORMAT_TEMPLATE_PATH env var / -t flag path: "" + + # if true, uses the go structs for the syft-json format for templating. + # if false, uses the syft-json output for templating (which follows the syft JSON schema exactly). + # + # Note: long term support for this option is not guaranteed (it may change or break at any time). + # SYFT_FORMAT_TEMPLATE_LEGACY env var + legacy: false # all spdx-json format options spdx-json: # include space indention and newlines # note: inherits default value from 'format.pretty' or 'false' if parent is unset + # SYFT_FORMAT_SPDX_JSON_PRETTY env var pretty: false # all cyclonedx-json format options @@ -593,6 +604,7 @@ format: # include space indention and newlines # note: inherits default value from 'format.pretty' or 'false' if parent is unset + # SYFT_FORMAT_CYCLONEDX_JSON_PRETTY env var pretty: false # all cyclonedx-xml format options @@ -600,6 +612,7 @@ format: # include space indention # note: inherits default value from 'format.pretty' or 'false' if parent is unset + # SYFT_FORMAT_CYCLONEDX_XML_PRETTY env var pretty: false diff --git a/cmd/syft/internal/options/format_template.go b/cmd/syft/internal/options/format_template.go index cd994187e..998709089 100644 --- a/cmd/syft/internal/options/format_template.go +++ b/cmd/syft/internal/options/format_template.go @@ -10,6 +10,7 @@ var _ clio.FlagAdder = (*FormatTemplate)(nil) type FormatTemplate struct { Enabled bool `yaml:"-" json:"-" mapstructure:"-"` Path string `yaml:"path" json:"path" mapstructure:"path"` // -t template file to use for output + Legacy bool `yaml:"legacy" json:"legacy" mapstructure:"legacy"` } func DefaultFormatTemplate() FormatTemplate { @@ -28,5 +29,6 @@ func (o *FormatTemplate) AddFlags(flags clio.FlagSet) { func (o FormatTemplate) config() template.EncoderConfig { return template.EncoderConfig{ TemplatePath: o.Path, + Legacy: o.Legacy, } } diff --git a/syft/format/encoders.go b/syft/format/encoders.go index 71af0705f..8bc38d98e 100644 --- a/syft/format/encoders.go +++ b/syft/format/encoders.go @@ -72,9 +72,7 @@ func (o EncodersConfig) Encoders() ([]sbom.FormatEncoder, error) { } func (o EncodersConfig) templateEncoders() ([]sbom.FormatEncoder, error) { - enc, err := template.NewFormatEncoder(template.EncoderConfig{ - TemplatePath: o.Template.TemplatePath, - }) + enc, err := template.NewFormatEncoder(o.Template) return []sbom.FormatEncoder{enc}, err } diff --git a/syft/format/template/encoder.go b/syft/format/template/encoder.go index 793b0ea5e..67822f109 100644 --- a/syft/format/template/encoder.go +++ b/syft/format/template/encoder.go @@ -1,6 +1,8 @@ package template import ( + "bytes" + "encoding/json" "errors" "fmt" "io" @@ -19,6 +21,7 @@ const ID sbom.FormatID = "template" type EncoderConfig struct { TemplatePath string + Legacy bool syftjson.EncoderConfig } @@ -90,6 +93,28 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error { return fmt.Errorf("unable to parse template: %w", err) } - doc := syftjson.ToFormatModel(s, e.cfg.EncoderConfig) + var doc any + if e.cfg.Legacy { + doc = syftjson.ToFormatModel(s, e.cfg.EncoderConfig) + } else { + enc, err := syftjson.NewFormatEncoderWithConfig(e.cfg.EncoderConfig) + if err != nil { + return fmt.Errorf("unable to create json encoder: %w", err) + } + + var buff bytes.Buffer + if err = enc.Encode(&buff, s); err != nil { + return fmt.Errorf("unable to encode json: %w", err) + } + + deserializedDoc := make(map[string]any) + + if err = json.Unmarshal(buff.Bytes(), &deserializedDoc); err != nil { + return fmt.Errorf("unable to unmarshal json for template: %w", err) + } + + doc = deserializedDoc + } + return tmpl.Execute(writer, doc) } diff --git a/syft/format/template/encoder_test.go b/syft/format/template/encoder_test.go index 976659e9f..2ed775a08 100644 --- a/syft/format/template/encoder_test.go +++ b/syft/format/template/encoder_test.go @@ -12,6 +12,42 @@ import ( var updateSnapshot = flag.Bool("update-template", false, "update the *.golden files for json encoders") +func TestFormatWithOption_Legacy(t *testing.T) { + f, err := NewFormatEncoder(EncoderConfig{ + TemplatePath: "test-fixtures/legacy/csv.template", + Legacy: true, + }) + require.NoError(t, err) + + testutil.AssertEncoderAgainstGoldenSnapshot(t, + testutil.EncoderSnapshotTestConfig{ + Subject: testutil.DirectoryInput(t, t.TempDir()), + Format: f, + UpdateSnapshot: *updateSnapshot, + PersistRedactionsInSnapshot: true, + IsJSON: false, + }, + ) +} + +func TestFormatWithOptionAndHasField_Legacy(t *testing.T) { + f, err := NewFormatEncoder(EncoderConfig{ + TemplatePath: "test-fixtures/legacy/csv-hasField.template", + Legacy: true, + }) + require.NoError(t, err) + + testutil.AssertEncoderAgainstGoldenSnapshot(t, + testutil.EncoderSnapshotTestConfig{ + Subject: testutil.DirectoryInputWithAuthorField(t), + Format: f, + UpdateSnapshot: *updateSnapshot, + PersistRedactionsInSnapshot: true, + IsJSON: false, + }, + ) +} + func TestFormatWithOption(t *testing.T) { f, err := NewFormatEncoder(EncoderConfig{ TemplatePath: "test-fixtures/csv.template", @@ -44,7 +80,6 @@ func TestFormatWithOptionAndHasField(t *testing.T) { IsJSON: false, }, ) - } func TestFormatWithoutOptions(t *testing.T) { diff --git a/syft/format/template/test-fixtures/csv-hasField.template b/syft/format/template/test-fixtures/csv-hasField.template index ad5ddb77f..2f7045b50 100644 --- a/syft/format/template/test-fixtures/csv-hasField.template +++ b/syft/format/template/test-fixtures/csv-hasField.template @@ -1,4 +1,4 @@ "Package","Version Installed","Found by","Author" -{{- range .Artifacts}} -"{{.Name}}","{{.Version}}","{{.FoundBy}}","{{ if hasField .Metadata "Author" }}{{.Metadata.Author}}{{ else }}NO AUTHOR SUPPLIED{{end}}" +{{- range .artifacts}} +"{{.name}}","{{.version}}","{{.foundBy}}","{{ if index .metadata "author" }}{{.metadata.author}}{{ else }}NO AUTHOR SUPPLIED{{end}}" {{- end}} \ No newline at end of file diff --git a/syft/format/template/test-fixtures/csv.template b/syft/format/template/test-fixtures/csv.template index 8474271ad..84ecbb40b 100644 --- a/syft/format/template/test-fixtures/csv.template +++ b/syft/format/template/test-fixtures/csv.template @@ -1,4 +1,4 @@ "Package","Version Installed", "Found by" -{{- range .Artifacts}} -"{{.Name}}","{{.Version}}","{{.FoundBy}}" +{{- range .artifacts}} +"{{.name}}","{{.version}}","{{.foundBy}}" {{- end}} \ No newline at end of file diff --git a/syft/format/template/test-fixtures/legacy/csv-hasField.template b/syft/format/template/test-fixtures/legacy/csv-hasField.template new file mode 100644 index 000000000..ad5ddb77f --- /dev/null +++ b/syft/format/template/test-fixtures/legacy/csv-hasField.template @@ -0,0 +1,4 @@ +"Package","Version Installed","Found by","Author" +{{- range .Artifacts}} +"{{.Name}}","{{.Version}}","{{.FoundBy}}","{{ if hasField .Metadata "Author" }}{{.Metadata.Author}}{{ else }}NO AUTHOR SUPPLIED{{end}}" +{{- end}} \ No newline at end of file diff --git a/syft/format/template/test-fixtures/legacy/csv.template b/syft/format/template/test-fixtures/legacy/csv.template new file mode 100644 index 000000000..8474271ad --- /dev/null +++ b/syft/format/template/test-fixtures/legacy/csv.template @@ -0,0 +1,4 @@ +"Package","Version Installed", "Found by" +{{- range .Artifacts}} +"{{.Name}}","{{.Version}}","{{.FoundBy}}" +{{- end}} \ No newline at end of file diff --git a/syft/format/template/test-fixtures/snapshot/TestFormatWithOptionAndHasField_Legacy.golden b/syft/format/template/test-fixtures/snapshot/TestFormatWithOptionAndHasField_Legacy.golden new file mode 100644 index 000000000..89c9fd10b --- /dev/null +++ b/syft/format/template/test-fixtures/snapshot/TestFormatWithOptionAndHasField_Legacy.golden @@ -0,0 +1,3 @@ +"Package","Version Installed","Found by","Author" +"package-1","1.0.1","the-cataloger-1","test-author" +"package-2","2.0.1","the-cataloger-2","NO AUTHOR SUPPLIED" \ No newline at end of file diff --git a/syft/format/template/test-fixtures/snapshot/TestFormatWithOption_Legacy.golden b/syft/format/template/test-fixtures/snapshot/TestFormatWithOption_Legacy.golden new file mode 100644 index 000000000..a7bc3b7d3 --- /dev/null +++ b/syft/format/template/test-fixtures/snapshot/TestFormatWithOption_Legacy.golden @@ -0,0 +1,3 @@ +"Package","Version Installed", "Found by" +"package-1","1.0.1","the-cataloger-1" +"package-2","2.0.1","the-cataloger-2" \ No newline at end of file diff --git a/test/cli/test-fixtures/csv.template b/test/cli/test-fixtures/csv.template index 8474271ad..84ecbb40b 100644 --- a/test/cli/test-fixtures/csv.template +++ b/test/cli/test-fixtures/csv.template @@ -1,4 +1,4 @@ "Package","Version Installed", "Found by" -{{- range .Artifacts}} -"{{.Name}}","{{.Version}}","{{.FoundBy}}" +{{- range .artifacts}} +"{{.name}}","{{.version}}","{{.foundBy}}" {{- end}} \ No newline at end of file