Skip to content

Commit

Permalink
Add support for spdx expression (#304)
Browse files Browse the repository at this point in the history
Signed-off-by: harisudarsan1 <sudarshanhari561@gmail.com>

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 <sudarshanhari561@gmail.com>

test: Add test case for spdx license evaluator

chore: Update go.mod

chore: Update vulnerable dependencies
  • Loading branch information
harisudarsan1 authored Jan 6, 2025
1 parent 3fab469 commit 6b050fe
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 27 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
63 changes: 56 additions & 7 deletions pkg/analyzer/filter/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package filter
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"

Expand All @@ -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 (
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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)
}
}
86 changes: 86 additions & 0 deletions pkg/analyzer/filter/eval_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}

0 comments on commit 6b050fe

Please sign in to comment.