mirror of
https://github.com/safedep/vet.git
synced 2025-12-10 13:43:01 -06:00
Merge pull request #20 from safedep/develop
Add Support for Filter Suite
This commit is contained in:
commit
8873a351ab
3
.github/workflows/codeql.yml
vendored
3
.github/workflows/codeql.yml
vendored
@ -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]')"
|
||||
|
||||
3
.github/workflows/container.yml
vendored
3
.github/workflows/container.yml
vendored
@ -7,6 +7,9 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
4
.github/workflows/secret_scan.yml
vendored
4
.github/workflows/secret_scan.yml
vendored
@ -3,6 +3,10 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trufflehog:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -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
|
||||
|
||||
6
Makefile
6
Makefile
@ -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)"
|
||||
|
||||
18
README.md
18
README.md
@ -8,6 +8,9 @@ source dependencies and evaluate them against organizational policies.
|
||||
[](https://github.com/safedep/vet/actions/workflows/codeql.yml)
|
||||
[](https://github.com/safedep/vet/actions/workflows/scorecard.yml)
|
||||
|
||||
## Demo
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
14
api/filter_suite_spec.proto
Normal file
14
api/filter_suite_spec.proto
Normal 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;
|
||||
}
|
||||
@ -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?
|
||||
|
||||
236
gen/filtersuite/filter_suite_spec.pb.go
Normal file
236
gen/filtersuite/filter_suite_spec.pb.go
Normal 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
3
go.mod
@ -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
11
go.sum
@ -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=
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
155
pkg/analyzer/cel_filter_suite.go
Normal file
155
pkg/analyzer/cel_filter_suite.go
Normal 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
|
||||
}
|
||||
57
pkg/analyzer/cel_filter_suite_test.go
Normal file
57
pkg/analyzer/cel_filter_suite_test.go
Normal 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()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
pkg/analyzer/cel_filter_utils.go
Normal file
55
pkg/analyzer/cel_filter_utils.go
Normal 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
261
pkg/analyzer/filter/eval.go
Normal 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
|
||||
}
|
||||
12
pkg/analyzer/filter/program.go
Normal file
12
pkg/analyzer/filter/program.go
Normal 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
|
||||
}
|
||||
18
pkg/analyzer/filter/result.go
Normal file
18
pkg/analyzer/filter/result.go
Normal 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
|
||||
}
|
||||
2
pkg/analyzer/fixtures/filter_suite_invalid.yml
Normal file
2
pkg/analyzer/fixtures/filter_suite_invalid.yml
Normal file
@ -0,0 +1,2 @@
|
||||
A: 1
|
||||
B: 2
|
||||
5
pkg/analyzer/fixtures/filter_suite_valid.yml
Normal file
5
pkg/analyzer/fixtures/filter_suite_valid.yml
Normal file
@ -0,0 +1,5 @@
|
||||
name: Valid Filter Suite
|
||||
description: Valid Filter Suite
|
||||
filters:
|
||||
- name: r1
|
||||
value: "true"
|
||||
@ -26,7 +26,7 @@ const (
|
||||
summaryWeightUnpopular = 2
|
||||
summaryWeightMajorDrift = 2
|
||||
|
||||
tagVuln = "vulnerabiity"
|
||||
tagVuln = "vulnerability"
|
||||
tagUnpopular = "low popularity"
|
||||
tagDrift = "drift"
|
||||
|
||||
|
||||
13
query.go
13
query.go
@ -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 {
|
||||
|
||||
22
samples/filter-suites/fs-generic.yml
Normal file
22
samples/filter-suites/fs-generic.yml
Normal 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
13
scan.go
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user