mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +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).
|
- `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.
|
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||||
- `table`: A columnar summary (default).
|
- `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
|
#### Multiple outputs
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
type PackagesOptions struct {
|
type PackagesOptions struct {
|
||||||
Scope string
|
Scope string
|
||||||
Output []string
|
Output []string
|
||||||
|
OutputTemplatePath string
|
||||||
File string
|
File string
|
||||||
Platform string
|
Platform string
|
||||||
Host 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),
|
cmd.PersistentFlags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
|
||||||
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
|
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", "", "",
|
cmd.PersistentFlags().StringVarP(&o.File, "file", "", "",
|
||||||
"file to write the default report output to (default is STDOUT)")
|
"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
|
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 {
|
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/formats/table"
|
"github.com/anchore/syft/internal/formats/table"
|
||||||
|
"github.com/anchore/syft/internal/formats/template"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/hashicorp/go-multierror"
|
"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
|
// 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
|
// 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) {
|
func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
||||||
outputOptions, err := parseOutputs(outputs, defaultFile)
|
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
// always should have one option -- we generally get the default of "table", but just make sure
|
||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
outputs = append(outputs, string(table.ID))
|
outputs = append(outputs, string(table.ID))
|
||||||
@ -56,6 +57,11 @@ func parseOutputs(outputs []string, defaultFile string) (out []sbom.WriterOption
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tmpl, ok := format.(template.OutputFormat); ok {
|
||||||
|
tmpl.SetTemplatePath(templateFilePath)
|
||||||
|
format = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
out = append(out, sbom.NewWriterOption(format, file))
|
out = append(out, sbom.NewWriterOption(format, file))
|
||||||
}
|
}
|
||||||
return out, errs
|
return out, errs
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func TestIsSupportedFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
_, err := MakeWriter(tt.outputs, "")
|
_, err := MakeWriter(tt.outputs, "", "")
|
||||||
tt.wantErr(t, err)
|
tt.wantErr(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,12 +14,13 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
|
{{.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 -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
{{.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:
|
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.
|
{{.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/anchore"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
|
"github.com/anchore/syft/internal/formats/template"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
@ -25,7 +26,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -185,3 +191,19 @@ func runPackageSbomUpload(src *source.Source, s sbom.SBOM, app *config.Applicati
|
|||||||
|
|
||||||
return nil
|
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>
|
const powerUserExample = ` {{.appName}} {{.command}} <image>
|
||||||
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
|
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)
|
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 (
|
require (
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.2
|
||||||
github.com/docker/docker v20.10.12+incompatible
|
github.com/docker/docker v20.10.12+incompatible
|
||||||
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839
|
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839
|
||||||
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
|
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/autorest/date v0.3.0 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // 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/Microsoft/go-winio v0.5.2 // indirect
|
||||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1 // 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/go-retryablehttp v0.7.1 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // 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/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // 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/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
github.com/miekg/pkcs11 v1.1.1 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/nwaples/rardecode v1.1.0 // 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/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||||
github.com/shibumi/go-pathspec v1.3.0 // 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/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||||
github.com/soheilhy/cmux v0.1.5 // 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/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/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.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/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.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver v1.5.0/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.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.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/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.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||||
github.com/Masterminds/sprig v2.22.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/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.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
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.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.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.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/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/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
|
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 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||||
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
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/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/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 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=
|
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)
|
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
|
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
|
||||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
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
|
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
|
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
|
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/spdx22tagvalue"
|
||||||
"github.com/anchore/syft/internal/formats/syftjson"
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
"github.com/anchore/syft/internal/formats/table"
|
"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/internal/formats/text"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
@ -25,6 +26,7 @@ const (
|
|||||||
GitHubID = github.ID
|
GitHubID = github.ID
|
||||||
SPDXTagValueFormatID = spdx22tagvalue.ID
|
SPDXTagValueFormatID = spdx22tagvalue.ID
|
||||||
SPDXJSONFormatID = spdx22json.ID
|
SPDXJSONFormatID = spdx22json.ID
|
||||||
|
TemplateFormatID = template.ID
|
||||||
)
|
)
|
||||||
|
|
||||||
var formats []sbom.Format
|
var formats []sbom.Format
|
||||||
@ -39,6 +41,7 @@ func init() {
|
|||||||
spdx22json.Format(),
|
spdx22json.Format(),
|
||||||
table.Format(),
|
table.Format(),
|
||||||
text.Format(),
|
text.Format(),
|
||||||
|
template.Format(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +87,8 @@ func FormatByName(name string) sbom.Format {
|
|||||||
return FormatByID(table.ID)
|
return FormatByID(table.ID)
|
||||||
case "text":
|
case "text":
|
||||||
return FormatByID(text.ID)
|
return FormatByID(text.ID)
|
||||||
|
case "template":
|
||||||
|
FormatByID(template.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
||||||
"github.com/anchore/syft/internal/formats/syftjson"
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
"github.com/anchore/syft/internal/formats/table"
|
"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/internal/formats/text"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -181,6 +182,11 @@ func TestFormatByName(t *testing.T) {
|
|||||||
name: "github-json",
|
name: "github-json",
|
||||||
want: github.ID,
|
want: github.ID,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
want: template.ID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|||||||
@ -2,10 +2,12 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/template"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +25,12 @@ func TestAllFormatsExpressible(t *testing.T) {
|
|||||||
require.NotEmpty(t, formats)
|
require.NotEmpty(t, formats)
|
||||||
for _, o := range formats {
|
for _, o := range formats {
|
||||||
t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) {
|
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 {
|
for _, traitFn := range commonAssertions {
|
||||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
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