mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
add template output (#1051)
* add template output Signed-off-by: Jonas Xavier <jonasx@anchore.com> * remove dead code Signed-off-by: Jonas Xavier <jonasx@anchore.com> * fix template cli flag Signed-off-by: Jonas Xavier <jonasx@anchore.com> * implement template's own format type Signed-off-by: Jonas Xavier <jonasx@anchore.com> * simpler code Signed-off-by: Jonas Xavier <jonasx@anchore.com> * fix readme link to Go template Signed-off-by: Jonas Xavier <jonasx@anchore.com> * feedback changes Signed-off-by: Jonas Xavier <jonasx@anchore.com> * simpler func signature patter Signed-off-by: Jonas Xavier <jonasx@anchore.com> * nit Signed-off-by: Jonas Xavier <jonasx@anchore.com> * fix linter error Signed-off-by: Jonas Xavier <jonasx@anchore.com>
This commit is contained in:
parent
03e37044d4
commit
aed1599c4d
34
README.md
34
README.md
@ -230,6 +230,40 @@ Where the `formats` available are:
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||
- `table`: A columnar summary (default).
|
||||
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
||||
|
||||
#### Using templates
|
||||
|
||||
Syft lets you define custom output formats, using [Go templates](https://pkg.go.dev/text/template). Here's how it works:
|
||||
|
||||
- Define your format as a Go template, and save this template as a file.
|
||||
|
||||
- Set the output format to "template" (`-o template`).
|
||||
|
||||
- Specify the path to the template file (`-t ./path/to/custom.template`).
|
||||
|
||||
- Syft's template processing uses the same data models as the `json` output format — so if you're wondering what data is available as you author a template, you can use the output from `syft <image> -o json` as a reference.
|
||||
|
||||
**Example:** You could make Syft output data in CSV format by writing a Go template that renders CSV data and then running `syft <image> -o template -t ~/path/to/csv.tmpl`.
|
||||
|
||||
Here's what the `csv.tmpl` file might look like:
|
||||
```gotemplate
|
||||
"Package","Version Installed","Found by"
|
||||
{{- range .Artifacts}}
|
||||
"{{.Name}}","{{.Version}}","{{.FoundBy}}"
|
||||
{{- end}}
|
||||
```
|
||||
|
||||
Which would produce output like:
|
||||
```text
|
||||
"Package","Version Installed","Found by"
|
||||
"alpine-baselayout","3.2.0-r20","apkdb-cataloger"
|
||||
"alpine-baselayout-data","3.2.0-r20","apkdb-cataloger"
|
||||
"alpine-keys","2.4-r1","apkdb-cataloger"
|
||||
...
|
||||
```
|
||||
|
||||
Syft also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default Golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output format.
|
||||
|
||||
#### Multiple outputs
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File)
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
type PackagesOptions struct {
|
||||
Scope string
|
||||
Output []string
|
||||
OutputTemplatePath string
|
||||
File string
|
||||
Platform string
|
||||
Host string
|
||||
@ -35,6 +36,9 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
cmd.PersistentFlags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
|
||||
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&o.OutputTemplatePath, "template", "t", "",
|
||||
"specify the path to a Go template file")
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&o.File, "file", "", "",
|
||||
"file to write the default report output to (default is STDOUT)")
|
||||
|
||||
@ -84,6 +88,10 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("output-template-path", flags.Lookup("template")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
"github.com/anchore/syft/internal/formats/template"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -12,8 +13,8 @@ import (
|
||||
|
||||
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
||||
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
|
||||
func MakeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
|
||||
outputOptions, err := parseOutputs(outputs, defaultFile)
|
||||
func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
||||
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -27,7 +28,7 @@ func MakeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
|
||||
}
|
||||
|
||||
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||
func parseOutputs(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) {
|
||||
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
|
||||
// always should have one option -- we generally get the default of "table", but just make sure
|
||||
if len(outputs) == 0 {
|
||||
outputs = append(outputs, string(table.ID))
|
||||
@ -56,6 +57,11 @@ func parseOutputs(outputs []string, defaultFile string) (out []sbom.WriterOption
|
||||
continue
|
||||
}
|
||||
|
||||
if tmpl, ok := format.(template.OutputFormat); ok {
|
||||
tmpl.SetTemplatePath(templateFilePath)
|
||||
format = tmpl
|
||||
}
|
||||
|
||||
out = append(out, sbom.NewWriterOption(format, file))
|
||||
}
|
||||
return out, errs
|
||||
|
||||
@ -28,7 +28,7 @@ func TestIsSupportedFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := MakeWriter(tt.outputs, "")
|
||||
_, err := MakeWriter(tt.outputs, "", "")
|
||||
tt.wantErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ const (
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/formats/template"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
@ -25,7 +26,12 @@ import (
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File)
|
||||
err := validateOutputOptions(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -185,3 +191,19 @@ func runPackageSbomUpload(src *source.Source, s sbom.SBOM, app *config.Applicati
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOutputOptions(app *config.Application) error {
|
||||
var usesTemplateOutput bool
|
||||
for _, o := range app.Outputs {
|
||||
if o == template.ID.String() {
|
||||
usesTemplateOutput = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if usesTemplateOutput && app.OutputTemplatePath == "" {
|
||||
return fmt.Errorf(`must specify path to template file when using "template" output format`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
|
||||
const powerUserExample = ` {{.appName}} {{.command}} <image>
|
||||
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
|
||||
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
|
||||
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported, template outputs are not supported.
|
||||
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
|
||||
`
|
||||
|
||||
|
||||
7
go.mod
7
go.mod
@ -54,6 +54,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
|
||||
@ -79,6 +80,8 @@ require (
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
||||
@ -175,6 +178,7 @@ require (
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
@ -197,6 +201,8 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
@ -218,6 +224,7 @@ require (
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
|
||||
5
go.sum
5
go.sum
@ -186,14 +186,17 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v1.27.0/go.mod h1:bn9iHmAjogMoIPkqBGyJ9R1m9cXGCjBE/cuhBs3oEsQ=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
@ -1302,6 +1305,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
|
||||
@ -1856,6 +1860,7 @@ github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAx
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
||||
github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
|
||||
@ -38,6 +38,7 @@ type Application struct {
|
||||
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
|
||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
|
||||
49
internal/formats/template/encoder.go
Normal file
49
internal/formats/template/encoder.go
Normal file
@ -0,0 +1,49 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
func makeTemplateExecutor(templateFilePath string) (*template.Template, error) {
|
||||
if templateFilePath == "" {
|
||||
return nil, errors.New("no template file: please provide a template path")
|
||||
}
|
||||
|
||||
expandedPathToTemplateFile, err := homedir.Expand(templateFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to expand path %s", templateFilePath)
|
||||
}
|
||||
|
||||
templateContents, err := os.ReadFile(expandedPathToTemplateFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get template content: %w", err)
|
||||
}
|
||||
|
||||
templateName := expandedPathToTemplateFile
|
||||
tmpl, err := template.New(templateName).Funcs(funcMap).Parse(string(templateContents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse template: %w", err)
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// These are custom functions available to template authors.
|
||||
var funcMap = func() template.FuncMap {
|
||||
f := sprig.HermeticTxtFuncMap()
|
||||
f["getLastIndex"] = func(collection interface{}) int {
|
||||
if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice {
|
||||
return v.Len() - 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}()
|
||||
29
internal/formats/template/encoder_test.go
Normal file
29
internal/formats/template/encoder_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var updateTmpl = flag.Bool("update-tmpl", false, "update the *.golden files for json encoders")
|
||||
|
||||
func TestFormatWithOption(t *testing.T) {
|
||||
f := OutputFormat{}
|
||||
f.SetTemplatePath("test-fixtures/csv.template")
|
||||
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
f,
|
||||
testutils.DirectoryInput(t),
|
||||
*updateTmpl,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestFormatWithoutOptions(t *testing.T) {
|
||||
f := Format()
|
||||
err := f.Encode(nil, testutils.DirectoryInput(t))
|
||||
assert.ErrorContains(t, err, "no template file: please provide a template path")
|
||||
}
|
||||
47
internal/formats/template/format.go
Normal file
47
internal/formats/template/format.go
Normal file
@ -0,0 +1,47 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID sbom.FormatID = "template"
|
||||
|
||||
func Format() sbom.Format {
|
||||
return OutputFormat{}
|
||||
}
|
||||
|
||||
// implementation of sbom.Format interface
|
||||
// to make use of format options
|
||||
type OutputFormat struct {
|
||||
templateFilePath string
|
||||
}
|
||||
|
||||
func (f OutputFormat) ID() sbom.FormatID {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (f OutputFormat) Decode(reader io.Reader) (*sbom.SBOM, error) {
|
||||
return nil, sbom.ErrDecodingNotSupported
|
||||
}
|
||||
|
||||
func (f OutputFormat) Encode(output io.Writer, s sbom.SBOM) error {
|
||||
tmpl, err := makeTemplateExecutor(f.templateFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc := syftjson.ToFormatModel(s)
|
||||
return tmpl.Execute(output, doc)
|
||||
}
|
||||
|
||||
func (f OutputFormat) Validate(reader io.Reader) error {
|
||||
return sbom.ErrValidationNotSupported
|
||||
}
|
||||
|
||||
// SetTemplatePath sets path for template file
|
||||
func (f *OutputFormat) SetTemplatePath(filePath string) {
|
||||
f.templateFilePath = filePath
|
||||
}
|
||||
4
internal/formats/template/test-fixtures/csv.template
Normal file
4
internal/formats/template/test-fixtures/csv.template
Normal file
@ -0,0 +1,4 @@
|
||||
"Package","Version Installed", "Found by"
|
||||
{{- range .Artifacts}}
|
||||
"{{.Name}}","{{.Version}}","{{.FoundBy}}"
|
||||
{{- end}}
|
||||
@ -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"
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
"github.com/anchore/syft/internal/formats/template"
|
||||
"github.com/anchore/syft/internal/formats/text"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
@ -25,6 +26,7 @@ const (
|
||||
GitHubID = github.ID
|
||||
SPDXTagValueFormatID = spdx22tagvalue.ID
|
||||
SPDXJSONFormatID = spdx22json.ID
|
||||
TemplateFormatID = template.ID
|
||||
)
|
||||
|
||||
var formats []sbom.Format
|
||||
@ -39,6 +41,7 @@ func init() {
|
||||
spdx22json.Format(),
|
||||
table.Format(),
|
||||
text.Format(),
|
||||
template.Format(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,6 +87,8 @@ func FormatByName(name string) sbom.Format {
|
||||
return FormatByID(table.ID)
|
||||
case "text":
|
||||
return FormatByID(text.ID)
|
||||
case "template":
|
||||
FormatByID(template.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
"github.com/anchore/syft/internal/formats/template"
|
||||
"github.com/anchore/syft/internal/formats/text"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -181,6 +182,11 @@ func TestFormatByName(t *testing.T) {
|
||||
name: "github-json",
|
||||
want: github.ID,
|
||||
},
|
||||
|
||||
{
|
||||
name: "template",
|
||||
want: template.ID,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@ -2,10 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/template"
|
||||
"github.com/anchore/syft/syft"
|
||||
)
|
||||
|
||||
@ -23,7 +25,12 @@ func TestAllFormatsExpressible(t *testing.T) {
|
||||
require.NotEmpty(t, formats)
|
||||
for _, o := range formats {
|
||||
t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) {
|
||||
cmd, stdout, stderr := runSyft(t, nil, "dir:./test-fixtures/image-pkg-coverage", "-o", string(o))
|
||||
args := []string{"dir:./test-fixtures/image-pkg-coverage", "-o", string(o)}
|
||||
if o == template.ID {
|
||||
args = append(args, "-t", "test-fixtures/csv.template")
|
||||
}
|
||||
|
||||
cmd, stdout, stderr := runSyft(t, nil, args...)
|
||||
for _, traitFn := range commonAssertions {
|
||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
||||
4
test/cli/test-fixtures/csv.template
Normal file
4
test/cli/test-fixtures/csv.template
Normal file
@ -0,0 +1,4 @@
|
||||
"Package","Version Installed", "Found by"
|
||||
{{- range .Artifacts}}
|
||||
"{{.Name}}","{{.Version}}","{{.FoundBy}}"
|
||||
{{- end}}
|
||||
Loading…
x
Reference in New Issue
Block a user