Merge pull request #20 from safedep/develop

Add Support for Filter Suite
This commit is contained in:
Abhisek Datta 2023-02-19 22:01:53 +05:30 committed by GitHub
commit 8873a351ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 994 additions and 222 deletions

View File

@ -18,6 +18,9 @@ on:
# The branches below must be a subset of the branches above
branches: [ "main" ]
permissions:
contents: read
jobs:
analyze:
if: "!contains(github.event.commits[0].message, '[noci]')"

View File

@ -7,6 +7,9 @@ on:
branches:
- "main"
permissions:
contents: read
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

View File

@ -3,6 +3,10 @@ on:
pull_request:
branches:
- main
permissions:
contents: read
jobs:
trufflehog:
runs-on: ubuntu-latest

View File

@ -20,6 +20,7 @@ LABEL org.opencontainers.image.source=https://github.com/safedep/vet
LABEL org.opencontainers.image.description="Open source software supply chain security tool"
LABEL org.opencontainers.image.licenses=Apache-2.0
COPY ./samples/ /vet/samples
COPY --from=build /build/vet /usr/local/bin/vet
USER nonroot:nonroot

View File

@ -21,9 +21,13 @@ protoc-codegen:
--go_out=./gen/filterinput \
--go_opt=paths=source_relative \
./api/filter_input_spec.proto
protoc -I ./api \
--go_out=./gen/filtersuite \
--go_opt=paths=source_relative \
./api/filter_suite_spec.proto
setup:
mkdir -p out gen/insightapi gen/controlplane gen/filterinput
mkdir -p out gen/insightapi gen/controlplane gen/filterinput gen/filtersuite
GO_CFLAGS=-X main.commit=$(GITCOMMIT) -X main.version=$(VERSION)
GO_LDFLAGS=-ldflags "-w $(GO_CFLAGS)"

View File

@ -8,6 +8,9 @@ source dependencies and evaluate them against organizational policies.
[![CodeQL](https://github.com/safedep/vet/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/safedep/vet/actions/workflows/codeql.yml)
[![Scorecard supply-chain security](https://github.com/safedep/vet/actions/workflows/scorecard.yml/badge.svg)](https://github.com/safedep/vet/actions/workflows/scorecard.yml)
## Demo
[![asciicast](https://asciinema.org/a/I60aD2VtVsETQtIFsYTCewJZ3.svg)](https://asciinema.org/a/I60aD2VtVsETQtIFsYTCewJZ3)
## TL;DR
@ -79,14 +82,21 @@ vet scan --lockfiles /path/to/pom.xml --report-summary=false \
> Use filtering along with `query` command for offline slicing and dicing of
> enriched package manifests. Read [filtering guide](docs/filtering.md)
[Common Expressions Language](https://github.com/google/cel-spec) is used to
evaluate filters on packages. Learn more about [filtering with vet](docs/filtering.md).
Learn more about [filtering with vet](docs/filtering.md).
Look at [filter input spec](api/filter_input_spec.proto) on attributes
available to the filter expression.
## Policy Evaluation
### Using Filter Suite
TODO
Filter suites can be used to implement security gating in CI. [Example](samples/filter-suites/fs-generic.yml)
file suite contains rules to enforce generic OSS consumption best practices.
```bash
vet scan -D /path/to/dir --filter-suite /path/to/suite.yml --filter-fail
```
Read more about filter suites in [filtering guide](docs/filtering.md)
## FAQ

View File

@ -0,0 +1,14 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/filtersuite";
message Filter {
string name = 1;
string value = 2;
}
message FilterSuite {
string name = 1;
string description = 2;
repeated Filter filters = 3;
}

View File

@ -12,6 +12,8 @@ vet scan -D /path/to/repo \
--filter 'licenses.exists(p, p == "MIT")'
```
The scan will list only packages that use the `MIT` license.
## Input
Filter expressions work on packages (aka. dependencies) and evaluates to
@ -29,19 +31,23 @@ Filter expressions get the following input data to work with
| `licenses` | Holds a list of liceses in SPDX license code format |
Refer to [filter input spec](../api/filter_input_spec.proto) for detailed
structure of input messages.
## Expressions
Expressions are [CEL](https://github.com/google/cel-spec) statements. While
CEL internals are not required, an [introductory](https://github.com/google/cel-spec/blob/master/doc/intro.md)
knowledge of CEL will help formulating queries.
knowledge of CEL will help formulating queries. Expressions are logical
statements that evaluate to `true` or `false`.
### Example Queries
| Description | Query |
|----------------------------------------------|---------------------------------------|
| Find packages with a critical vulnerability | `vulns.critical.exists(x, true)` |
| Find unmaintained packages as per OpenSSF SC | `scorecard.score["Maintained"] == 0` |
| Find packages with low stars | `projects.exists(x, x.stars < 10)` |
| Description | Query |
|----------------------------------------------|--------------------------------------|
| Find packages with a critical vulnerability | `vulns.critical.exists(x, true)` |
| Find unmaintained packages as per OpenSSF SC | `scorecard.scores.Maintained == 0` |
| Find packages with low stars | `projects.exists(x, x.stars < 10)` |
| Find packages with GPL-2.0 license | `licenses.exists(x, x == "GPL-2.0")`
Refer to [scorecard checks](https://github.com/ossf/scorecard#checks-1) for
@ -69,7 +75,7 @@ vet query --from /tmp/dump --report-summary
vet query --from /tmp/dump --filter 'scorecard.score.Maintained == 0'
```
## Gating with Filters
## Security Gating with Filters
A simple security gate (in CI) can be achieved using the filters. The
`--filter-fail` argument tells the `Filter Analyzer` module to fail the command
@ -90,6 +96,57 @@ Subsequently, the command fails with `-1` exit code in case of match
255
```
## Filter Suite
A single filter is useful for identification of packages that meet some
specific criteria. While it helps solve various use-cases, it is not entirely
suitable for `security gating` where multiple filters may be required to
express an organization's acceptable OSS usage policy.
For example, an organization may define a filter to deny certain type of
packages:
1. Any package that has a high or critical vulnerability
2. Any package that does not match acceptable OSS licenses
3. Any package that has a low [OpenSSF scorecard score](https://github.com/ossf/scorecard)
To express this policy, multiple filters are needed such as:
```
vulns.critical.exists(p, true) ||
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0")) ||
(scorecard.scores.Maintained == 0)
```
To solve this problem, we introduce the concept of `Filter Suite`. It can be
represented as an YAML file containing multiple filters to match:
```yaml
name: Generic Filter Suite
description: Example filter suite with canned filters
filters:
- name: critical-vuln
value: |
vulns.critical.exists(p, true)
- name: safe-licenses
value: |
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0"))
- name: ossf-maintained
value: |
scorecard.scores.Maintained == 0
```
A scan or query operation can be invoked using the filter suite:
```bash
vet scan -D /path/to/repo --filter-suite /path/to/filters.yml --filter-fail
```
The filter suite will be evaluated as:
* Ordered list of filters as given in the suite file
* Stop on first rule match for a given package
## FAQ
### How does the filter input JSON look like?

View File

@ -0,0 +1,236 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.18.0
// source: filter_suite_spec.proto
package filtersuite
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Filter struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Filter) Reset() {
*x = Filter{}
if protoimpl.UnsafeEnabled {
mi := &file_filter_suite_spec_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Filter) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Filter) ProtoMessage() {}
func (x *Filter) ProtoReflect() protoreflect.Message {
mi := &file_filter_suite_spec_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Filter.ProtoReflect.Descriptor instead.
func (*Filter) Descriptor() ([]byte, []int) {
return file_filter_suite_spec_proto_rawDescGZIP(), []int{0}
}
func (x *Filter) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Filter) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
type FilterSuite struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
Filters []*Filter `protobuf:"bytes,3,rep,name=filters,proto3" json:"filters,omitempty"`
}
func (x *FilterSuite) Reset() {
*x = FilterSuite{}
if protoimpl.UnsafeEnabled {
mi := &file_filter_suite_spec_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *FilterSuite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FilterSuite) ProtoMessage() {}
func (x *FilterSuite) ProtoReflect() protoreflect.Message {
mi := &file_filter_suite_spec_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FilterSuite.ProtoReflect.Descriptor instead.
func (*FilterSuite) Descriptor() ([]byte, []int) {
return file_filter_suite_spec_proto_rawDescGZIP(), []int{1}
}
func (x *FilterSuite) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *FilterSuite) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *FilterSuite) GetFilters() []*Filter {
if x != nil {
return x.Filters
}
return nil
}
var File_filter_suite_spec_proto protoreflect.FileDescriptor
var file_filter_suite_spec_proto_rawDesc = []byte{
0x0a, 0x17, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x75, 0x69, 0x74, 0x65, 0x5f, 0x73,
0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x32, 0x0a, 0x06, 0x46, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x66, 0x0a,
0x0b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x07, 0x66, 0x69,
0x6c, 0x74, 0x65, 0x72, 0x73, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x61, 0x66, 0x65, 0x64, 0x65, 0x70, 0x2f, 0x76, 0x65, 0x74, 0x2f,
0x67, 0x65, 0x6e, 0x2f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x75, 0x69, 0x74, 0x65, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_filter_suite_spec_proto_rawDescOnce sync.Once
file_filter_suite_spec_proto_rawDescData = file_filter_suite_spec_proto_rawDesc
)
func file_filter_suite_spec_proto_rawDescGZIP() []byte {
file_filter_suite_spec_proto_rawDescOnce.Do(func() {
file_filter_suite_spec_proto_rawDescData = protoimpl.X.CompressGZIP(file_filter_suite_spec_proto_rawDescData)
})
return file_filter_suite_spec_proto_rawDescData
}
var file_filter_suite_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_filter_suite_spec_proto_goTypes = []interface{}{
(*Filter)(nil), // 0: Filter
(*FilterSuite)(nil), // 1: FilterSuite
}
var file_filter_suite_spec_proto_depIdxs = []int32{
0, // 0: FilterSuite.filters:type_name -> Filter
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_filter_suite_spec_proto_init() }
func file_filter_suite_spec_proto_init() {
if File_filter_suite_spec_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_filter_suite_spec_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Filter); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_filter_suite_spec_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FilterSuite); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_filter_suite_spec_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_filter_suite_spec_proto_goTypes,
DependencyIndexes: file_filter_suite_spec_proto_depIdxs,
MessageInfos: file_filter_suite_spec_proto_msgTypes,
}.Build()
File_filter_suite_spec_proto = out.File
file_filter_suite_spec_proto_rawDesc = nil
file_filter_suite_spec_proto_goTypes = nil
file_filter_suite_spec_proto_depIdxs = nil
}

3
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/google/cel-go v0.13.0
github.com/google/osv-scanner v1.1.0
github.com/jedib0t/go-pretty/v6 v6.4.4
github.com/safedep/dry v0.0.0-20230216112435-385c68e56634
github.com/safedep/dry v0.0.0-20230218045153-1a93b0397b55
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
@ -37,4 +37,5 @@ require (
golang.org/x/text v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

11
go.sum
View File

@ -8,12 +8,15 @@ github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s=
github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -43,10 +46,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safedep/dry v0.0.0-20230203134955-367834d99b1c h1:zbhTBT463mwcIuCq89GT8pFTU8UtGalBWFaa/wsgVXA=
github.com/safedep/dry v0.0.0-20230203134955-367834d99b1c/go.mod h1:yZ8R6kv4pR0yertVoxgBmnN4bvHT8TLubE7aahpWDDk=
github.com/safedep/dry v0.0.0-20230216112435-385c68e56634 h1:JRIzwT2Xo7TFH2O1gMJpHS5Fn6jDJdF+/+2JdyhzI3A=
github.com/safedep/dry v0.0.0-20230216112435-385c68e56634/go.mod h1:yZ8R6kv4pR0yertVoxgBmnN4bvHT8TLubE7aahpWDDk=
github.com/safedep/dry v0.0.0-20230218045153-1a93b0397b55 h1:OBzggSWzjyEa7YaXp2DvpKDe1wYXtOEcFXQfDqkK7PI=
github.com/safedep/dry v0.0.0-20230218045153-1a93b0397b55/go.mod h1:odFOtG1l46k23IaCY6kdNkkLW8L+NT+EUVYYVphP59I=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
@ -92,3 +93,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -3,8 +3,15 @@ package ui
import (
"fmt"
"os"
"github.com/jedib0t/go-pretty/v6/text"
)
func PrintBanner(s string) {
fmt.Fprintf(os.Stderr, s)
}
func PrintError(s string, args ...any) {
msg := fmt.Sprintf(s, args...)
fmt.Fprint(os.Stderr, text.FgRed.Sprint(msg), "\n")
}

View File

@ -88,7 +88,7 @@ func redirectLogToFile(path string) {
func failOnError(stage string, err error) {
if err != nil {
logger.Errorf("%s failed due to error %v", stage, err)
ui.PrintError("%s failed due to error: %s", stage, err.Error())
os.Exit(-1)
}
}

View File

@ -1,73 +1,40 @@
package analyzer
import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"github.com/golang/protobuf/jsonpb"
"github.com/google/cel-go/cel"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/gen/filterinput"
"github.com/safedep/vet/gen/insightapi"
"github.com/safedep/vet/pkg/analyzer/filter"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
)
const (
filterInputVarRoot = "_"
filterInputVarPkg = "pkg"
filterInputVarVulns = "vulns"
filterInputVarScorecard = "scorecard"
filterInputVarProjects = "projects"
filterInputVarLicenses = "licenses"
)
type celFilterAnalyzer struct {
program cel.Program
evaluator filter.Evaluator
failOnMatch bool
packages map[string]*models.Package
stat struct {
manifests int
packages int
matched int
err int
}
stat celFilterStat
}
func NewCelFilterAnalyzer(filter string, failOnMatch bool) (Analyzer, error) {
env, err := cel.NewEnv(
cel.Variable(filterInputVarPkg, cel.DynType),
cel.Variable(filterInputVarVulns, cel.DynType),
cel.Variable(filterInputVarProjects, cel.DynType),
cel.Variable(filterInputVarScorecard, cel.DynType),
cel.Variable(filterInputVarLicenses, cel.DynType),
cel.Variable(filterInputVarRoot, cel.DynType),
)
func NewCelFilterAnalyzer(fl string, failOnMatch bool) (Analyzer, error) {
evaluator, err := filter.NewEvaluator("single-filter", true)
if err != nil {
return nil, err
}
ast, issues := env.Compile(filter)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
prog, err := env.Program(ast)
err = evaluator.AddFilter("single-filter", fl)
if err != nil {
return nil, err
}
return &celFilterAnalyzer{program: prog,
return &celFilterAnalyzer{
evaluator: evaluator,
failOnMatch: failOnMatch,
packages: make(map[string]*models.Package),
stat: celFilterStat{},
}, nil
}
@ -79,51 +46,29 @@ func (f *celFilterAnalyzer) Analyze(manifest *models.PackageManifest,
handler AnalyzerEventHandler) error {
logger.Infof("CEL filtering manifest: %s", manifest.Path)
f.stat.manifests += 1
f.stat.IncScannedManifest()
for _, pkg := range manifest.Packages {
f.stat.packages += 1
f.stat.IncEvaluatedPackage()
filterInput, err := f.buildFilterInput(pkg)
res, err := f.evaluator.EvalPackage(pkg)
if err != nil {
f.stat.err += 1
logger.Errorf("Failed to convert package to filter input: %v", err)
continue
}
f.stat.IncError(err)
serializedInput, err := f.serializeFilterInput(filterInput)
if err != nil {
f.stat.err += 1
logger.Errorf("Failed to serialize filter input: %v", err)
continue
}
out, _, err := f.program.Eval(map[string]interface{}{
filterInputVarRoot: serializedInput,
filterInputVarPkg: serializedInput["pkg"],
filterInputVarProjects: serializedInput["projects"],
filterInputVarVulns: serializedInput["vulns"],
filterInputVarScorecard: serializedInput["scorecard"],
filterInputVarLicenses: serializedInput["licenses"],
})
if err != nil {
f.stat.err += 1
logger.Errorf("Failed to evaluate CEL for %s:%v : %v",
logger.Errorf("Failed to evaluate CEL for %s:%s : %v",
pkg.PackageDetails.Name,
pkg.PackageDetails.Version, err)
continue
}
if (reflect.TypeOf(out).Kind() == reflect.Bool) &&
(reflect.ValueOf(out).Bool()) {
if res.Matched() {
// Avoid duplicates added to the table
if _, ok := f.packages[pkg.Id()]; ok {
continue
}
f.stat.matched += 1
f.stat.IncMatchedPackage()
f.packages[pkg.Id()] = pkg
}
}
@ -146,10 +91,7 @@ func (f *celFilterAnalyzer) Finish() error {
})
}
fmt.Printf("%s\n", text.Bold.Sprint("Filter evaluated with ",
f.stat.matched, " out of ", f.stat.packages, " uniquely matched and ",
f.stat.err, " error(s) ", "across ", f.stat.manifests,
" manifest(s)"))
f.stat.PrintStatMessage(os.Stderr)
tbl.Render()
return nil
@ -157,7 +99,7 @@ func (f *celFilterAnalyzer) Finish() error {
func (f *celFilterAnalyzer) notifyCaller(manifest *models.PackageManifest,
handler AnalyzerEventHandler) error {
if f.failOnMatch && (f.stat.matched > 0) {
if f.failOnMatch && (len(f.packages) > 0) {
handler(&AnalyzerEvent{
Source: f.Name(),
Type: ET_AnalyzerFailOnError,
@ -170,27 +112,6 @@ func (f *celFilterAnalyzer) notifyCaller(manifest *models.PackageManifest,
return nil
}
// TODO: Fix this JSON round-trip problem by directly configuring CEL env to
// work with Protobuf messages
func (f *celFilterAnalyzer) serializeFilterInput(fi *filterinput.FilterInput) (map[string]interface{}, error) {
var ret map[string]interface{}
m := jsonpb.Marshaler{OrigName: true, EnumsAsInts: false, EmitDefaults: true}
data, err := m.MarshalToString(fi)
if err != nil {
return ret, err
}
logger.Debugf("Serialized filter input: %s", data)
err = json.Unmarshal([]byte(data), &ret)
if err != nil {
return ret, err
}
return ret, nil
}
func (f *celFilterAnalyzer) pkgLatestVersion(pkg *models.Package) string {
insight := utils.SafelyGetValue(pkg.Insights)
return utils.SafelyGetValue(insight.PackageCurrentVersion)
@ -206,108 +127,3 @@ func (f *celFilterAnalyzer) pkgSource(pkg *models.Package) string {
return ""
}
func (f *celFilterAnalyzer) buildFilterInput(pkg *models.Package) (*filterinput.FilterInput, error) {
fi := filterinput.FilterInput{
Pkg: &filterinput.PackageVersion{
Ecosystem: strings.ToLower(string(pkg.PackageDetails.Ecosystem)),
Name: pkg.PackageDetails.Name,
Version: pkg.PackageDetails.Version,
},
Projects: []*filterinput.ProjectInfo{},
Vulns: &filterinput.Vulnerabilities{
All: []*filterinput.Vulnerability{},
Critical: []*filterinput.Vulnerability{},
High: []*filterinput.Vulnerability{},
Medium: []*filterinput.Vulnerability{},
Low: []*filterinput.Vulnerability{},
},
Scorecard: &filterinput.Scorecard{
Scores: map[string]float32{},
},
Licenses: []string{},
}
// Safely get insight
insight := utils.SafelyGetValue(pkg.Insights)
// Add projects
projectTypeMapper := func(tp string) filterinput.ProjectType {
tp = strings.ToLower(tp)
if tp == "github" {
return filterinput.ProjectType_GITHUB
} else {
return filterinput.ProjectType_UNKNOWN
}
}
for _, project := range utils.SafelyGetValue(insight.Projects) {
fi.Projects = append(fi.Projects, &filterinput.ProjectInfo{
Name: utils.SafelyGetValue(project.Name),
Stars: int32(utils.SafelyGetValue(project.Stars)),
Forks: int32(utils.SafelyGetValue(project.Forks)),
Issues: int32(utils.SafelyGetValue(project.Issues)),
Type: projectTypeMapper(utils.SafelyGetValue(project.Type)),
})
}
// Add vulnerabilities
cveFilter := func(aliases []string) string {
for _, alias := range aliases {
if strings.HasPrefix(strings.ToUpper(alias), "CVE-") {
return alias
}
}
return ""
}
for _, vuln := range utils.SafelyGetValue(insight.Vulnerabilities) {
fiv := filterinput.Vulnerability{
Id: utils.SafelyGetValue(vuln.Id),
Cve: cveFilter(utils.SafelyGetValue(vuln.Aliases)),
}
fi.Vulns.All = append(fi.Vulns.All, &fiv)
risk := insightapi.PackageVulnerabilitySeveritiesRiskUNKNOWN
for _, s := range utils.SafelyGetValue(vuln.Severities) {
sType := utils.SafelyGetValue(s.Type)
if (sType == insightapi.PackageVulnerabilitySeveritiesTypeCVSSV3) ||
(sType == insightapi.PackageVulnerabilitySeveritiesTypeCVSSV2) {
risk = utils.SafelyGetValue(s.Risk)
break
}
}
switch risk {
case insightapi.PackageVulnerabilitySeveritiesRiskCRITICAL:
fi.Vulns.Critical = append(fi.Vulns.Critical, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskHIGH:
fi.Vulns.High = append(fi.Vulns.High, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskMEDIUM:
fi.Vulns.Medium = append(fi.Vulns.Medium, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskLOW:
fi.Vulns.Low = append(fi.Vulns.Low, &fiv)
break
}
}
// Add licenses
for _, lic := range utils.SafelyGetValue(insight.Licenses) {
fi.Licenses = append(fi.Licenses, string(lic))
}
// Scorecard
scorecard := utils.SafelyGetValue(insight.Scorecard)
checks := utils.SafelyGetValue(utils.SafelyGetValue(scorecard.Content).Checks)
for _, check := range checks {
fi.Scorecard.Scores[string(utils.SafelyGetValue(check.Name))] =
utils.SafelyGetValue(check.Score)
}
return &fi, nil
}

View File

@ -0,0 +1,155 @@
package analyzer
import (
"fmt"
"os"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/gen/filtersuite"
"github.com/safedep/vet/pkg/analyzer/filter"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
)
type celFilterMatchedPackage struct {
pkg *models.Package
filterName string
}
type celFilterSuiteAnalyzer struct {
evaluator filter.Evaluator
suite *filtersuite.FilterSuite
failOnMatch bool
matchedPackages map[string]*celFilterMatchedPackage
stat celFilterStat
}
func NewCelFilterSuiteAnalyzer(path string, failOnMatch bool) (Analyzer, error) {
fs, err := loadFilterSuiteFromFile(path)
if err != nil {
return nil, err
}
evaluator, err := filter.NewEvaluator(fs.GetName(), true)
if err != nil {
return nil, err
}
for _, fl := range fs.GetFilters() {
err = evaluator.AddFilter(fl.GetName(), fl.GetValue())
if err != nil {
return nil, err
}
}
return &celFilterSuiteAnalyzer{
evaluator: evaluator,
suite: fs,
failOnMatch: failOnMatch,
matchedPackages: make(map[string]*celFilterMatchedPackage),
stat: celFilterStat{},
}, nil
}
func (f *celFilterSuiteAnalyzer) Name() string {
return "CEL Filter Suite"
}
func (f *celFilterSuiteAnalyzer) Analyze(manifest *models.PackageManifest,
handler AnalyzerEventHandler) error {
logger.Infof("CEL Filter Suite: Analyzing manifest: %s", manifest.Path)
f.stat.IncScannedManifest()
for _, pkg := range manifest.Packages {
f.stat.IncEvaluatedPackage()
res, err := f.evaluator.EvalPackage(pkg)
if err != nil {
f.stat.IncError(err)
logger.Errorf("Failed to evaluate CEL for %s:%s : %v",
pkg.PackageDetails.Name,
pkg.PackageDetails.Version, err)
continue
}
if res.Matched() {
f.queueMatchedPkg(pkg, res.GetMatchedFilter().Name())
}
}
if f.failOnMatch && (len(f.matchedPackages) > 0) {
handler(&AnalyzerEvent{
Source: f.Name(),
Type: ET_AnalyzerFailOnError,
Manifest: manifest,
Err: fmt.Errorf("failed due to filter suite match on %s", manifest.Path),
})
}
return nil
}
func (f *celFilterSuiteAnalyzer) Finish() error {
f.renderMatchTable()
return nil
}
func (f *celFilterSuiteAnalyzer) renderMatchTable() {
tbl := table.NewWriter()
tbl.SetStyle(table.StyleLight)
tbl.SetOutputMirror(os.Stdout)
tbl.AppendHeader(table.Row{"Ecosystem", "Package", "Latest",
"Filter"})
for _, mp := range f.matchedPackages {
insights := utils.SafelyGetValue(mp.pkg.Insights)
tbl.AppendRow(table.Row{
mp.pkg.PackageDetails.Ecosystem,
fmt.Sprintf("%s@%s", mp.pkg.PackageDetails.Name,
mp.pkg.PackageDetails.Version),
utils.SafelyGetValue(insights.PackageCurrentVersion),
mp.filterName,
})
}
f.stat.PrintStatMessage(os.Stderr)
tbl.Render()
}
func (f *celFilterSuiteAnalyzer) queueMatchedPkg(pkg *models.Package,
filterName string) {
if _, ok := f.matchedPackages[pkg.Id()]; ok {
return
}
f.stat.IncMatchedPackage()
f.matchedPackages[pkg.Id()] = &celFilterMatchedPackage{
filterName: filterName,
pkg: pkg,
}
}
// To correctly unmarshal a []byte into protobuf message, we must use
// protobuf SDK and not generic JSON / YAML decoder. Since there is no
// officially supported yamlpb, equivalent to jsonpb, we convert YAML
// to JSON before unmarshalling it into a protobuf message
func loadFilterSuiteFromFile(path string) (*filtersuite.FilterSuite, error) {
logger.Debugf("CEL Filter Suite: Loading suite from file: %s", path)
file, err := os.Open(path)
if err != nil {
return nil, err
}
var msg filtersuite.FilterSuite
err = utils.FromYamlToPb(file, &msg)
if err != nil {
return nil, err
}
return &msg, nil
}

View File

@ -0,0 +1,57 @@
package analyzer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadFilterSuiteFromFile(t *testing.T) {
cases := []struct {
name string
path string
suiteName string
suiteDesc string
filtersCount int
errMsg string
}{
{
"valid filter suite",
"fixtures/filter_suite_valid.yml",
"Valid Filter Suite",
"Valid Filter Suite",
1,
"",
},
{
"invalid filter suite",
"fixtures/filter_suite_invalid.yml",
"",
"",
0,
"unknown field",
},
{
"filter suite does not exists",
"fixtures/filter_suite_does_not_exists.yml",
"",
"",
0,
"no such file or directory",
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
fs, err := loadFilterSuiteFromFile(test.path)
if test.errMsg != "" {
assert.NotNil(t, err)
assert.ErrorContains(t, err, test.errMsg)
} else {
assert.Equal(t, test.suiteName, fs.GetName())
assert.Equal(t, test.suiteDesc, fs.GetDescription())
assert.Equal(t, test.filtersCount, len(fs.GetFilters()))
}
})
}
}

View File

@ -0,0 +1,55 @@
package analyzer
import (
"fmt"
"io"
"github.com/jedib0t/go-pretty/v6/text"
)
type celFilterStat struct {
evaluatedManifests int
evaluatedPackages int
matchedPackages int
errCount int
}
func (s *celFilterStat) IncScannedManifest() {
s.evaluatedManifests += 1
}
func (s *celFilterStat) IncEvaluatedPackage() {
s.evaluatedPackages += 1
}
func (s *celFilterStat) IncMatchedPackage() {
s.matchedPackages += 1
}
func (s *celFilterStat) IncError(_ error) {
s.errCount += 1
}
func (s *celFilterStat) EvaluatedManifests() int {
return s.evaluatedManifests
}
func (s *celFilterStat) EvaluatedPackages() int {
return s.evaluatedPackages
}
func (s *celFilterStat) MatchedPackages() int {
return s.matchedPackages
}
func (s *celFilterStat) ErrorCount() int {
return s.errCount
}
func (s *celFilterStat) PrintStatMessage(writer io.Writer) {
fmt.Fprintf(writer, "%s\n", text.Bold.Sprint("Filter evaluated with ",
s.matchedPackages, " out of ", s.evaluatedPackages, " uniquely matched and ",
s.errCount, " error(s) ", "across ", s.evaluatedManifests,
" manifest(s)"))
}

261
pkg/analyzer/filter/eval.go Normal file
View File

@ -0,0 +1,261 @@
package filter
import (
"encoding/json"
"errors"
"reflect"
"strings"
"github.com/golang/protobuf/jsonpb"
"github.com/google/cel-go/cel"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/gen/filterinput"
"github.com/safedep/vet/gen/insightapi"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
)
const (
filterInputVarRoot = "_"
filterInputVarPkg = "pkg"
filterInputVarVulns = "vulns"
filterInputVarScorecard = "scorecard"
filterInputVarProjects = "projects"
filterInputVarLicenses = "licenses"
// Soft limit to start with
filterEvalMaxFilters = 50
)
var (
errMaxFilter = errors.New("max filter limit has reached")
)
type Evaluator interface {
AddFilter(name, filter string) error
EvalPackage(pkg *models.Package) (*filterEvaluationResult, error)
}
type filterEvaluator struct {
name string
env *cel.Env
programs []*filterProgram
ignoreError bool
}
func NewEvaluator(name string, ignoreError bool) (Evaluator, error) {
env, err := cel.NewEnv(
cel.Variable(filterInputVarPkg, cel.DynType),
cel.Variable(filterInputVarVulns, cel.DynType),
cel.Variable(filterInputVarProjects, cel.DynType),
cel.Variable(filterInputVarScorecard, cel.DynType),
cel.Variable(filterInputVarLicenses, cel.DynType),
cel.Variable(filterInputVarRoot, cel.DynType),
)
if err != nil {
return nil, err
}
return &filterEvaluator{
name: name,
env: env,
programs: []*filterProgram{},
ignoreError: ignoreError,
}, nil
}
func (f *filterEvaluator) AddFilter(name, filter string) error {
if len(f.programs) >= filterEvalMaxFilters {
return errMaxFilter
}
ast, issues := f.env.Compile(filter)
if issues != nil && issues.Err() != nil {
return issues.Err()
}
prog, err := f.env.Program(ast)
if err != nil {
return err
}
f.programs = append(f.programs, &filterProgram{
name: name,
program: prog,
})
return nil
}
func (f *filterEvaluator) EvalPackage(pkg *models.Package) (*filterEvaluationResult, error) {
filterInput, err := f.buildFilterInput(pkg)
if err != nil {
return nil, err
}
serializedInput, err := f.serializeFilterInput(filterInput)
if err != nil {
return nil, err
}
for _, prog := range f.programs {
out, _, err := prog.program.Eval(map[string]interface{}{
filterInputVarRoot: serializedInput,
filterInputVarPkg: serializedInput["pkg"],
filterInputVarProjects: serializedInput["projects"],
filterInputVarVulns: serializedInput["vulns"],
filterInputVarScorecard: serializedInput["scorecard"],
filterInputVarLicenses: serializedInput["licenses"],
})
if err != nil {
logger.Warnf("CEL evaluator error: %s", err.Error())
if f.ignoreError {
continue
}
return nil, err
}
if (reflect.TypeOf(out).Kind() == reflect.Bool) &&
(reflect.ValueOf(out).Bool()) {
return &filterEvaluationResult{
match: true,
program: prog,
}, nil
}
}
return &filterEvaluationResult{
match: false,
}, nil
}
// TODO: Fix this JSON round-trip problem by directly configuring CEL env to
// work with Protobuf messages
func (f *filterEvaluator) serializeFilterInput(fi *filterinput.FilterInput) (map[string]interface{}, error) {
var ret map[string]interface{}
m := jsonpb.Marshaler{OrigName: true, EnumsAsInts: false, EmitDefaults: true}
data, err := m.MarshalToString(fi)
if err != nil {
return ret, err
}
logger.Debugf("Serialized filter input: %s", data)
err = json.Unmarshal([]byte(data), &ret)
if err != nil {
return ret, err
}
return ret, nil
}
func (f *filterEvaluator) buildFilterInput(pkg *models.Package) (*filterinput.FilterInput, error) {
fi := filterinput.FilterInput{
Pkg: &filterinput.PackageVersion{
Ecosystem: strings.ToLower(string(pkg.PackageDetails.Ecosystem)),
Name: pkg.PackageDetails.Name,
Version: pkg.PackageDetails.Version,
},
Projects: []*filterinput.ProjectInfo{},
Vulns: &filterinput.Vulnerabilities{
All: []*filterinput.Vulnerability{},
Critical: []*filterinput.Vulnerability{},
High: []*filterinput.Vulnerability{},
Medium: []*filterinput.Vulnerability{},
Low: []*filterinput.Vulnerability{},
},
Scorecard: &filterinput.Scorecard{
Scores: map[string]float32{},
},
Licenses: []string{},
}
// Safely get insight
insight := utils.SafelyGetValue(pkg.Insights)
// Add projects
projectTypeMapper := func(tp string) filterinput.ProjectType {
tp = strings.ToLower(tp)
if tp == "github" {
return filterinput.ProjectType_GITHUB
} else {
return filterinput.ProjectType_UNKNOWN
}
}
for _, project := range utils.SafelyGetValue(insight.Projects) {
fi.Projects = append(fi.Projects, &filterinput.ProjectInfo{
Name: utils.SafelyGetValue(project.Name),
Stars: int32(utils.SafelyGetValue(project.Stars)),
Forks: int32(utils.SafelyGetValue(project.Forks)),
Issues: int32(utils.SafelyGetValue(project.Issues)),
Type: projectTypeMapper(utils.SafelyGetValue(project.Type)),
})
}
// Add vulnerabilities
cveFilter := func(aliases []string) string {
for _, alias := range aliases {
if strings.HasPrefix(strings.ToUpper(alias), "CVE-") {
return alias
}
}
return ""
}
for _, vuln := range utils.SafelyGetValue(insight.Vulnerabilities) {
fiv := filterinput.Vulnerability{
Id: utils.SafelyGetValue(vuln.Id),
Cve: cveFilter(utils.SafelyGetValue(vuln.Aliases)),
}
fi.Vulns.All = append(fi.Vulns.All, &fiv)
risk := insightapi.PackageVulnerabilitySeveritiesRiskUNKNOWN
for _, s := range utils.SafelyGetValue(vuln.Severities) {
sType := utils.SafelyGetValue(s.Type)
if (sType == insightapi.PackageVulnerabilitySeveritiesTypeCVSSV3) ||
(sType == insightapi.PackageVulnerabilitySeveritiesTypeCVSSV2) {
risk = utils.SafelyGetValue(s.Risk)
break
}
}
switch risk {
case insightapi.PackageVulnerabilitySeveritiesRiskCRITICAL:
fi.Vulns.Critical = append(fi.Vulns.Critical, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskHIGH:
fi.Vulns.High = append(fi.Vulns.High, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskMEDIUM:
fi.Vulns.Medium = append(fi.Vulns.Medium, &fiv)
break
case insightapi.PackageVulnerabilitySeveritiesRiskLOW:
fi.Vulns.Low = append(fi.Vulns.Low, &fiv)
break
}
}
// Add licenses
for _, lic := range utils.SafelyGetValue(insight.Licenses) {
fi.Licenses = append(fi.Licenses, string(lic))
}
// Scorecard
scorecard := utils.SafelyGetValue(insight.Scorecard)
checks := utils.SafelyGetValue(utils.SafelyGetValue(scorecard.Content).Checks)
for _, check := range checks {
fi.Scorecard.Scores[string(utils.SafelyGetValue(check.Name))] =
utils.SafelyGetValue(check.Score)
}
return &fi, nil
}

View File

@ -0,0 +1,12 @@
package filter
import "github.com/google/cel-go/cel"
type filterProgram struct {
name string
program cel.Program
}
func (p *filterProgram) Name() string {
return p.name
}

View File

@ -0,0 +1,18 @@
package filter
type filterEvaluationResult struct {
match bool
program *filterProgram
}
func (r *filterEvaluationResult) Matched() bool {
return r.match
}
func (r *filterEvaluationResult) GetMatchedFilter() *filterProgram {
if r.program == nil {
return &filterProgram{}
}
return r.program
}

View File

@ -0,0 +1,2 @@
A: 1
B: 2

View File

@ -0,0 +1,5 @@
name: Valid Filter Suite
description: Valid Filter Suite
filters:
- name: r1
value: "true"

View File

@ -26,7 +26,7 @@ const (
summaryWeightUnpopular = 2
summaryWeightMajorDrift = 2
tagVuln = "vulnerabiity"
tagVuln = "vulnerability"
tagUnpopular = "low popularity"
tagDrift = "drift"

View File

@ -10,6 +10,7 @@ import (
var (
queryFilterExpression string
queryFilterSuiteFile string
queryFilterFailOnMatch bool
queryLoadDirectory string
queryEnableConsoleReport bool
@ -31,6 +32,8 @@ func newQueryCommand() *cobra.Command {
"The directory to load JSON dump files")
cmd.Flags().StringVarP(&queryFilterExpression, "filter", "", "",
"Filter and print packages using CEL")
cmd.Flags().StringVarP(&queryFilterSuiteFile, "filter-suite", "", "",
"Filter packages using CEL Filter Suite from file")
cmd.Flags().BoolVarP(&queryFilterFailOnMatch, "filter-fail", "", false,
"Fail the command if filter matches any package (for security gate)")
cmd.Flags().BoolVarP(&queryEnableConsoleReport, "report-console", "", false,
@ -61,6 +64,16 @@ func internalStartQuery() error {
analyzers = append(analyzers, task)
}
if !utils.IsEmptyString(queryFilterSuiteFile) {
task, err := analyzer.NewCelFilterSuiteAnalyzer(queryFilterSuiteFile,
queryFilterFailOnMatch)
if err != nil {
return err
}
analyzers = append(analyzers, task)
}
if queryEnableConsoleReport {
rp, err := reporter.NewConsoleReporter()
if err != nil {

View File

@ -0,0 +1,22 @@
name: General Purpose OSS Best Practices
description: |
This filter suite contains rules for implementing general purpose OSS
consumption best practices for an organization.
filters:
- name: critical-or-high-vulns
value: |
vulns.critical.exists(p, true) || vulns.high.exists(p, true)
- name: low-popularity
value: |
projects.exists(p, (p.type == "GITHUB") && (p.stars < 10))
- name: risky-oss-licenses
value: |
licenses.exists(p, p == "GPL-2.0") ||
licenses.exists(p, p == "GPL-3.0")
- name: ossf-unmaintained
value: |
scorecard.scores["Maintained"] == 0
- name: ossf-dangerous-workflow
value: |
scorecard.scores["Dangerous-Workflow"] == 0

13
scan.go
View File

@ -23,6 +23,7 @@ var (
concurrency int
dumpJsonManifestDir string
celFilterExpression string
celFilterSuiteFile string
celFilterFailOnMatch bool
markdownReportPath string
consoleReport bool
@ -63,6 +64,8 @@ func newScanCommand() *cobra.Command {
"Dump enriched package manifests as JSON files to dir")
cmd.Flags().StringVarP(&celFilterExpression, "filter", "", "",
"Filter and print packages using CEL")
cmd.Flags().StringVarP(&celFilterSuiteFile, "filter-suite", "", "",
"Filter packages using CEL Filter Suite from file")
cmd.Flags().BoolVarP(&celFilterFailOnMatch, "filter-fail", "", false,
"Fail the scan if the filter match any package (security gate)")
cmd.Flags().StringVarP(&markdownReportPath, "report-markdown", "", "",
@ -118,6 +121,16 @@ func internalStartScan() error {
analyzers = append(analyzers, task)
}
if !utils.IsEmptyString(celFilterSuiteFile) {
task, err := analyzer.NewCelFilterSuiteAnalyzer(celFilterSuiteFile,
celFilterFailOnMatch)
if err != nil {
return err
}
analyzers = append(analyzers, task)
}
reporters := []reporter.Reporter{}
if consoleReport {
rp, err := reporter.NewConsoleReporter()