From 6b050feee4d772b340572d60d99a70556eb09fa0 Mon Sep 17 00:00:00 2001 From: Harisudarsan <97289088+harisudarsan1@users.noreply.github.com> Date: Mon, 6 Jan 2025 08:09:35 +0530 Subject: [PATCH] Add support for spdx expression (#304) Signed-off-by: harisudarsan1 error handling and add examples for license filtering Handled the excluded errors in filter evaluator Examples are updated for the SPDX license filtering Signed-off-by: harisudarsan1 test: Add test case for spdx license evaluator chore: Update go.mod chore: Update vulnerable dependencies --- README.md | 19 ++++++- go.mod | 14 +++--- go.sum | 24 ++++----- pkg/analyzer/filter/eval.go | 63 ++++++++++++++++++++--- pkg/analyzer/filter/eval_test.go | 86 ++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 pkg/analyzer/filter/eval_test.go diff --git a/README.md b/README.md index 0b1d701a..323b9e9b 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ vet scan parsers --experimental (CEL) as the policy language. Policies can be defined to build guardrails preventing introduction of insecure components. +### Vulnerability + - Run `vet` and fail if a critical or high vulnerability was detected ```bash @@ -196,14 +198,29 @@ vet scan -D /path/to/code \ --filter-fail ``` +### License + - Run `vet` and fail if a package with a specific license was detected ```bash vet scan -D /path/to/code \ - --filter 'licenses.exists(p, p == "GPL-2.0")' \ + --filter 'licenses.exists(p, "GPL-2.0")' \ + --filter-fail +``` + +**Note:** Using `licenses.contains_license(...)` is recommended for license matching due +to its support for SPDX expressions. + +- `vet` supports [SPDX License Expressions](https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/) at package license and policy level + +```bash +vet scan -D /path/to/code \ + --filter 'licenses.contains_license("LGPL-2.1+")' \ --filter-fail ``` +### Scorecard + - Run `vet` and fail based on [OpenSSF Scorecard](https://securityscorecards.dev/) attributes ```bash diff --git a/go.mod b/go.mod index bab34d3b..886dbed2 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/cayleygraph/quad v1.3.0 github.com/cli/oauth v1.2.0 github.com/deepmap/oapi-codegen v1.16.3 + github.com/github/go-spdx/v2 v2.3.2 github.com/gofri/go-github-ratelimit v1.1.0 github.com/gojek/heimdall v5.0.2+incompatible github.com/gojek/heimdall/v7 v7.0.3 @@ -99,7 +100,6 @@ require ( github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.10.0 // indirect - github.com/github/go-spdx/v2 v2.3.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -216,14 +216,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.27.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect diff --git a/go.sum b/go.sum index 0fa9ffa4..e171a592 100644 --- a/go.sum +++ b/go.sum @@ -995,8 +995,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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= @@ -1083,8 +1083,8 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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= @@ -1116,8 +1116,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1191,12 +1191,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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= @@ -1207,8 +1207,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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/pkg/analyzer/filter/eval.go b/pkg/analyzer/filter/eval.go index e271cb7c..c7773cc2 100644 --- a/pkg/analyzer/filter/eval.go +++ b/pkg/analyzer/filter/eval.go @@ -3,6 +3,7 @@ package filter import ( "encoding/json" "errors" + "fmt" "reflect" "strings" @@ -15,6 +16,12 @@ import ( specmodels "github.com/safedep/vet/gen/models" "github.com/safedep/vet/pkg/common/logger" "github.com/safedep/vet/pkg/models" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" + + "github.com/github/go-spdx/v2/spdxexp" ) const ( @@ -31,9 +38,7 @@ const ( filterEvalMaxFilters = 50 ) -var ( - errMaxFilter = errors.New("max filter limit has reached") -) +var errMaxFilter = errors.New("max filter limit has reached") type Evaluator interface { AddFilter(filter *filtersuite.Filter) error @@ -55,7 +60,10 @@ func NewEvaluator(name string, ignoreError bool) (Evaluator, error) { cel.Variable(filterInputVarScorecard, cel.DynType), cel.Variable(filterInputVarLicenses, cel.DynType), cel.Variable(filterInputVarRoot, cel.DynType), - ) + cel.Function("contains_license", + cel.MemberOverload("list_string_contains_license_string", + []*cel.Type{cel.ListType(cel.StringType), cel.StringType}, cel.BoolType, + cel.BinaryBinding(celFuncLicenseExpressionMatch())))) if err != nil { return nil, err @@ -112,7 +120,6 @@ func (f *filterEvaluator) EvalPackage(pkg *models.Package) (*filterEvaluationRes filterInputVarScorecard: serializedInput["scorecard"], filterInputVarLicenses: serializedInput["licenses"], }) - if err != nil { logger.Warnf("CEL evaluator error: %s", err.Error()) @@ -262,9 +269,51 @@ func (f *filterEvaluator) buildFilterInput(pkg *models.Package) (*filterinput.Fi checks := utils.SafelyGetValue(scorecardContent.Checks) for _, check := range checks { - fi.Scorecard.Scores[string(utils.SafelyGetValue(check.Name))] = - utils.SafelyGetValue(check.Score) + fi.Scorecard.Scores[string(utils.SafelyGetValue(check.Name))] = utils.SafelyGetValue(check.Score) } return &fi, nil } + +func celFuncLicenseExpressionMatch() func(ref.Val, ref.Val) ref.Val { + return func(lhs, rhs ref.Val) ref.Val { + l, ok := lhs.(traits.Lister) + if !ok { + logger.Warnf("celFuncLicenseExpressionMatch: lhs is not a list") + return types.Bool(false) + } + + filterLicenseExp := fmt.Sprintf("%s", rhs) + iter := l.Iterator() + contains := false + + i := 0 + for { + if contains { + break + } + + if iter.HasNext().Value() == false { + break + } + + str := l.Get(types.Int(i)) + extracted, err := spdxexp.ExtractLicenses(fmt.Sprintf("%s", str)) + if err != nil { + logger.Errorf("error while extracting license exp: %v", err) + break + } + + satisfied, err := spdxexp.Satisfies(filterLicenseExp, extracted) + if err != nil { + logger.Errorf("error while checking license exp: %v", err) + break + } + + contains = satisfied + i++ + } + + return types.Bool(contains) + } +} diff --git a/pkg/analyzer/filter/eval_test.go b/pkg/analyzer/filter/eval_test.go new file mode 100644 index 00000000..633ccb41 --- /dev/null +++ b/pkg/analyzer/filter/eval_test.go @@ -0,0 +1,86 @@ +package filter + +import ( + "testing" + + "github.com/safedep/vet/gen/filtersuite" + "github.com/safedep/vet/gen/insightapi" + "github.com/safedep/vet/pkg/models" + "github.com/stretchr/testify/assert" +) + +func TestEvaluatorLicenseExpression(t *testing.T) { + cases := []struct { + name string + packageLicenses []string + filterString string + expected bool + skip bool + skipReason string + }{ + { + name: "License match by exists (current behavior)", + packageLicenses: []string{"MIT", "Apache-2.0"}, + filterString: "licenses.exists(p, p == 'MIT')", + expected: true, + }, + { + name: "Package has license expression does not match exists", + packageLicenses: []string{"MIT OR Apache-2.0"}, + filterString: "licenses.exists(p, p == 'MIT')", + expected: false, + }, + { + name: "Package has license expression matches expression", + packageLicenses: []string{"MIT OR Apache-2.0"}, + filterString: "licenses.contains_license('MIT')", + expected: true, + }, + { + name: "Package has license expression matches expression", + packageLicenses: []string{"MIT OR Apache-2.0"}, + filterString: "licenses.contains_license('Apache-2.0 OR MIT')", + expected: true, + }, + { + name: "Package has license expression does not match expression", + packageLicenses: []string{"MIT OR Apache-2.0"}, + filterString: "licenses.contains_license('Apache-2.0 AND MIT')", + expected: false, + skip: true, + skipReason: "AND expressions in filters are not supported yet", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if c.skip { + t.Skip(c.skipReason) + } + + f, err := NewEvaluator("test", false) + assert.NoError(t, err) + + err = f.AddFilter(&filtersuite.Filter{ + Name: "test", + Value: c.filterString, + }) + assert.NoError(t, err) + + licenses := []insightapi.License{} + for _, l := range c.packageLicenses { + licenses = append(licenses, insightapi.License(l)) + } + + pkg := &models.Package{ + Insights: &insightapi.PackageVersionInsight{ + Licenses: &licenses, + }, + } + + result, err := f.EvalPackage(pkg) + assert.NoError(t, err) + assert.Equal(t, c.expected, result.Matched()) + }) + } +}