diff --git a/Makefile b/Makefile index 852269117df..869357c25df 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ BOUNCER_VERSION := v0.4.0 CHRONICLE_VERSION := v0.7.0 GORELEASER_VERSION := v1.20.0 YAJSV_VERSION := v1.4.1 -COSIGN_VERSION := v2.1.1 +COSIGN_VERSION := v2.2.0 QUILL_VERSION := v0.4.1 GLOW_VERSION := v1.5.1 diff --git a/cmd/syft/cli/cli.go b/cmd/syft/cli/cli.go index b1347b274dd..91b7c5f8082 100644 --- a/cmd/syft/cli/cli.go +++ b/cmd/syft/cli/cli.go @@ -4,6 +4,7 @@ import ( "os" cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/stereoscope" @@ -15,12 +16,24 @@ import ( "github.com/anchore/syft/internal/redact" ) -// New constructs the `syft packages` command, aliases the root command to `syft packages`, +// Application constructs the `syft packages` command, aliases the root command to `syft packages`, // and constructs the `syft power-user` command. It is also responsible for // organizing flag usage and injecting the application config for each command. // It also constructs the syft attest command and the syft version command. // `RunE` is the earliest that the complete application configuration can be loaded. -func New(id clio.Identification) clio.Application { +func Application(id clio.Identification) clio.Application { + app, _ := create(id) + return app +} + +// Command returns the root command for the syft CLI application. This is useful for embedding the entire syft CLI +// into an existing application. +func Command(id clio.Identification) *cobra.Command { + _, cmd := create(id) + return cmd +} + +func create(id clio.Identification) (clio.Application, *cobra.Command) { clioCfg := clio.NewSetupConfig(id). WithGlobalConfigFlag(). // add persistent -c for reading an application config from WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config @@ -77,5 +90,5 @@ func New(id clio.Identification) clio.Application { cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane ) - return app + return app, rootCmd } diff --git a/cmd/syft/cli/commands/update.go b/cmd/syft/cli/commands/update.go index 3ef1812e1c8..88f15970035 100644 --- a/cmd/syft/cli/commands/update.go +++ b/cmd/syft/cli/commands/update.go @@ -38,7 +38,7 @@ func applicationUpdateCheck(id clio.Identification, check *options.UpdateCheck) func checkForApplicationUpdate(id clio.Identification) { log.Debugf("checking if a new version of %s is available", id.Name) - isAvailable, newVersion, err := isUpdateAvailable(id.Version) + isAvailable, newVersion, err := isUpdateAvailable(id) if err != nil { // this should never stop the application log.Errorf(err.Error()) @@ -59,18 +59,18 @@ func checkForApplicationUpdate(id clio.Identification) { } // isUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is. -func isUpdateAvailable(version string) (bool, string, error) { - if !isProductionBuild(version) { +func isUpdateAvailable(id clio.Identification) (bool, string, error) { + if !isProductionBuild(id.Version) { // don't allow for non-production builds to check for a version. return false, "", nil } - currentVersion, err := hashiVersion.NewVersion(version) + currentVersion, err := hashiVersion.NewVersion(id.Version) if err != nil { return false, "", fmt.Errorf("failed to parse current application version: %w", err) } - latestVersion, err := fetchLatestApplicationVersion() + latestVersion, err := fetchLatestApplicationVersion(id) if err != nil { return false, "", err } @@ -89,11 +89,12 @@ func isProductionBuild(version string) bool { return true } -func fetchLatestApplicationVersion() (*hashiVersion.Version, error) { +func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) { req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) if err != nil { return nil, fmt.Errorf("failed to create request for latest version: %w", err) } + req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version)) client := http.Client{} resp, err := client.Do(req) diff --git a/cmd/syft/cli/commands/update_test.go b/cmd/syft/cli/commands/update_test.go index 96cd7de34fc..bc04a74651f 100644 --- a/cmd/syft/cli/commands/update_test.go +++ b/cmd/syft/cli/commands/update_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "testing" + "github.com/anchore/clio" hashiVersion "github.com/anchore/go-version" "github.com/anchore/syft/cmd/syft/internal" ) @@ -106,7 +107,7 @@ func TestIsUpdateAvailable(t *testing.T) { t.Run(test.name, func(t *testing.T) { // setup mocks // local... - version := test.buildVersion + id := clio.Identification{Name: "Syft", Version: test.buildVersion} // remote... handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { @@ -117,7 +118,7 @@ func TestIsUpdateAvailable(t *testing.T) { latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() - isAvailable, newVersion, err := isUpdateAvailable(version) + isAvailable, newVersion, err := isUpdateAvailable(id) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { @@ -138,52 +139,67 @@ func TestIsUpdateAvailable(t *testing.T) { func TestFetchLatestApplicationVersion(t *testing.T) { tests := []struct { - name string - response string - code int - err bool - expected *hashiVersion.Version + name string + response string + code int + err bool + id clio.Identification + expected *hashiVersion.Version + expectedHeaders map[string]string }{ { - name: "gocase", - response: "1.0.0", - code: 200, - expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), + name: "gocase", + response: "1.0.0", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), + expectedHeaders: map[string]string{"User-Agent": "Syft 0.0.0"}, + err: false, }, { - name: "garbage", - response: "garbage", - code: 200, - expected: nil, - err: true, + name: "garbage", + response: "garbage", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "http 500", - response: "1.0.0", - code: 500, - expected: nil, - err: true, + name: "http 500", + response: "1.0.0", + code: 500, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "http 404", - response: "1.0.0", - code: 404, - expected: nil, - err: true, + name: "http 404", + response: "1.0.0", + code: 404, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "empty", - response: "", - code: 200, - expected: nil, - err: true, + name: "empty", + response: "", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "too long", - response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ", - code: 200, - expected: nil, - err: true, + name: "too long", + response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, } @@ -192,6 +208,15 @@ func TestFetchLatestApplicationVersion(t *testing.T) { // setup mock handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { + if test.expectedHeaders != nil { + for headerName, headerValue := range test.expectedHeaders { + actualHeader := r.Header.Get(headerName) + if actualHeader != headerValue { + t.Fatalf("expected header %v=%v but got %v", headerName, headerValue, actualHeader) + } + } + } + w.WriteHeader(test.code) _, _ = w.Write([]byte(test.response)) }) @@ -199,7 +224,7 @@ func TestFetchLatestApplicationVersion(t *testing.T) { latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() - actual, err := fetchLatestApplicationVersion() + actual, err := fetchLatestApplicationVersion(test.id) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { diff --git a/cmd/syft/main.go b/cmd/syft/main.go index c9a13881a2b..d1f303d0e22 100644 --- a/cmd/syft/main.go +++ b/cmd/syft/main.go @@ -20,7 +20,7 @@ var ( ) func main() { - app := cli.New( + app := cli.Application( clio.Identification{ Name: applicationName, Version: version, diff --git a/go.mod b/go.mod index 204049c773e..2daa04b3a7d 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/dave/jennifer v1.7.0 github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da github.com/docker/distribution v2.8.2+incompatible - github.com/docker/docker v24.0.5+incompatible + github.com/docker/docker v24.0.6+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 github.com/github/go-spdx/v2 v2.1.2 @@ -59,7 +59,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/pelletier/go-toml v1.9.5 github.com/pkg/errors v0.9.1 // indirect - github.com/saferwall/pe v1.4.4 + github.com/saferwall/pe v1.4.5 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/sassoftware/go-rpmutils v0.2.0 // pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5 @@ -78,11 +78,11 @@ require ( github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 github.com/xeipuuv/gojsonschema v1.2.0 github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 - golang.org/x/crypto v0.12.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/mod v0.12.0 - golang.org/x/net v0.14.0 - golang.org/x/term v0.11.0 + golang.org/x/net v0.15.0 + golang.org/x/term v0.12.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.25.0 ) @@ -184,8 +184,8 @@ require ( github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index 14c9908f1e9..39510151077 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qe github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= -github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -608,8 +608,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/saferwall/pe v1.4.4 h1:Ml++7/2/Z1iKwV4zCsd1nIqTEAdUQKAetwbbcCarhOg= -github.com/saferwall/pe v1.4.4/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= +github.com/saferwall/pe v1.4.5 h1:ACIe9QnLTdiRIbuN3BbEUI8SqCQmNrPBb7O2lJTmsK4= +github.com/saferwall/pe v1.4.5/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= @@ -768,8 +768,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -861,8 +861,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -976,16 +976,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -998,8 +998,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/syft/formats/common/cyclonedxhelpers/external_references.go b/syft/formats/common/cyclonedxhelpers/external_references.go index 59f388717a3..29770070a5a 100644 --- a/syft/formats/common/cyclonedxhelpers/external_references.go +++ b/syft/formats/common/cyclonedxhelpers/external_references.go @@ -2,6 +2,7 @@ package cyclonedxhelpers import ( "fmt" + "net/url" "strings" "github.com/CycloneDX/cyclonedx-go" @@ -15,9 +16,11 @@ import ( func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { var refs []cyclonedx.ExternalReference if hasMetadata(p) { + // Skip adding extracted URL and Homepage metadata + // as "external_reference" if the metadata isn't IRI-compliant switch metadata := p.Metadata.(type) { case pkg.ApkMetadata: - if metadata.URL != "" { + if metadata.URL != "" && isValidExternalRef(metadata.URL) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.URL, Type: cyclonedx.ERTypeDistribution, @@ -31,20 +34,20 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { }) } case pkg.NpmPackageJSONMetadata: - if metadata.URL != "" { + if metadata.URL != "" && isValidExternalRef(metadata.URL) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.URL, Type: cyclonedx.ERTypeDistribution, }) } - if metadata.Homepage != "" { + if metadata.Homepage != "" && isValidExternalRef(metadata.Homepage) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.Homepage, Type: cyclonedx.ERTypeWebsite, }) } case pkg.GemMetadata: - if metadata.Homepage != "" { + if metadata.Homepage != "" && isValidExternalRef(metadata.Homepage) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.Homepage, Type: cyclonedx.ERTypeWebsite, @@ -158,3 +161,9 @@ func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) str } return "" } + +// isValidExternalRef checks for IRI-comppliance for input string to be added into "external_reference" +func isValidExternalRef(s string) bool { + parsed, err := url.Parse(s) + return err == nil && parsed != nil && parsed.Host != "" +} diff --git a/syft/formats/common/cyclonedxhelpers/external_references_test.go b/syft/formats/common/cyclonedxhelpers/external_references_test.go index c6ce0355b4d..67fd73778da 100644 --- a/syft/formats/common/cyclonedxhelpers/external_references_test.go +++ b/syft/formats/common/cyclonedxhelpers/external_references_test.go @@ -32,7 +32,7 @@ func Test_encodeExternalReferences(t *testing.T) { }, }, { - name: "from npm", + name: "from npm with valid URL", input: pkg.Package{ Metadata: pkg.NpmPackageJSONMetadata{ URL: "http://a-place.gov", @@ -42,6 +42,18 @@ func Test_encodeExternalReferences(t *testing.T) { {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, }, }, + { + name: "from npm with invalid URL but valid Homepage", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "b-place", + Homepage: "http://b-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://b-place.gov", Type: cyclonedx.ERTypeWebsite}, + }, + }, { name: "from cargo lock", input: pkg.Package{ @@ -132,3 +144,32 @@ func Test_encodeExternalReferences(t *testing.T) { }) } } + +func Test_isValidExternalRef(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "valid URL for external_reference, git protocol", + input: "git+https://github.com/abc/def.git", + expected: true, + }, + { + name: "valid URL for external_reference, git protocol", + input: "git+https://github.com/abc/def.git", + expected: true, + }, + { + name: "invalid URL for external_reference", + input: "abc/def", + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, isValidExternalRef(test.input)) + }) + } +} diff --git a/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json b/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json index 7ffc387cc9e..a111bc0c3d7 100644 --- a/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json @@ -1059,6 +1059,7 @@ "pipreqs": "cpe:2.3:a:pipreqs_project:pipreqs:*:*:*:*:*:python:*:*", "proxy.py": "cpe:2.3:a:proxy.py_project:proxy.py:*:*:*:*:*:*:*:*", "py-bcrypt": "cpe:2.3:a:python:py-bcrypt:*:*:*:*:*:*:*:*", + "py-xml": "cpe:2.3:a:py-xml_project:py-xml:*:*:*:*:*:python:*:*", "py7zr": "cpe:2.3:a:py7zr_project:py7zr:*:*:*:*:*:python:*:*", "pybluemonday": "cpe:2.3:a:python:pybluemonday:*:*:*:*:*:*:*:*", "pycryptodome": "cpe:2.3:a:python:pycryptodome:*:*:*:*:*:*:*:*",