From 2ca64478cfdddf827aeea77148f46bc4c202322c Mon Sep 17 00:00:00 2001 From: abhisek Date: Sat, 18 Feb 2023 16:37:19 +0530 Subject: [PATCH] Add analysis rules for filter suite analyzer --- docs/filtering.md | 6 +-- pkg/analyzer/cel_filter.go | 2 +- pkg/analyzer/cel_filter_suite.go | 78 ++++++++++++++++++++++++--- pkg/analyzer/cel_filter_suite_test.go | 2 +- pkg/analyzer/filter/eval.go | 23 +++++--- pkg/analyzer/filter/result.go | 8 +++ samples/filter-suites/fs-generic.yml | 14 ++--- 7 files changed, 105 insertions(+), 28 deletions(-) diff --git a/docs/filtering.md b/docs/filtering.md index a215c1e..8ebc924 100644 --- a/docs/filtering.md +++ b/docs/filtering.md @@ -110,11 +110,7 @@ To express this policy, multiple filters are needed such as: ``` vulns.critical.exists(p, true) || - -licenses.exists(p, - (p != "MIT") && (p != "Apache-2.0") -) || - +licenses.exists(p, (p != "MIT") && (p != "Apache-2.0")) || (scorecard.scores.Maintained == 0) ``` diff --git a/pkg/analyzer/cel_filter.go b/pkg/analyzer/cel_filter.go index c46ed24..5a61ef3 100644 --- a/pkg/analyzer/cel_filter.go +++ b/pkg/analyzer/cel_filter.go @@ -27,7 +27,7 @@ type celFilterAnalyzer struct { } func NewCelFilterAnalyzer(fl string, failOnMatch bool) (Analyzer, error) { - evaluator, err := filter.NewEvaluator("single-filter") + evaluator, err := filter.NewEvaluator("single-filter", true) if err != nil { return nil, err } diff --git a/pkg/analyzer/cel_filter_suite.go b/pkg/analyzer/cel_filter_suite.go index 5343569..993d62d 100644 --- a/pkg/analyzer/cel_filter_suite.go +++ b/pkg/analyzer/cel_filter_suite.go @@ -1,18 +1,26 @@ 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/models" ) +type celFilterMatchedPackage struct { + pkg *models.Package + filterName string +} + type celFilterSuiteAnalyzer struct { - evaluator filter.Evaluator - suite *filtersuite.FilterSuite - failOnMatch bool + evaluator filter.Evaluator + suite *filtersuite.FilterSuite + failOnMatch bool + matchedPackages map[string]*celFilterMatchedPackage } func NewCelFilterSuiteAnalyzer(path string, failOnMatch bool) (Analyzer, error) { @@ -21,7 +29,7 @@ func NewCelFilterSuiteAnalyzer(path string, failOnMatch bool) (Analyzer, error) return nil, err } - evaluator, err := filter.NewEvaluator(fs.GetName()) + evaluator, err := filter.NewEvaluator(fs.GetName(), true) if err != nil { return nil, err } @@ -34,9 +42,10 @@ func NewCelFilterSuiteAnalyzer(path string, failOnMatch bool) (Analyzer, error) } return &celFilterSuiteAnalyzer{ - evaluator: evaluator, - suite: fs, - failOnMatch: failOnMatch, + evaluator: evaluator, + suite: fs, + failOnMatch: failOnMatch, + matchedPackages: make(map[string]*celFilterMatchedPackage), }, nil } @@ -46,13 +55,68 @@ func (f *celFilterSuiteAnalyzer) Name() string { func (f *celFilterSuiteAnalyzer) Analyze(manifest *models.PackageManifest, handler AnalyzerEventHandler) error { + + for _, pkg := range manifest.Packages { + res, err := f.evaluator.EvalPackage(pkg) + if err != nil { + 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, + }) + } + + tbl.Render() +} + +func (f *celFilterSuiteAnalyzer) queueMatchedPkg(pkg *models.Package, + filterName string) { + if _, ok := f.matchedPackages[pkg.Id()]; ok { + return + } + + 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 diff --git a/pkg/analyzer/cel_filter_suite_test.go b/pkg/analyzer/cel_filter_suite_test.go index 7a4f252..fdaba75 100644 --- a/pkg/analyzer/cel_filter_suite_test.go +++ b/pkg/analyzer/cel_filter_suite_test.go @@ -29,7 +29,7 @@ func TestLoadFilterSuiteFromFile(t *testing.T) { "", "", 0, - "unknown field \"A\" in FilterSuite", + "unknown field", }, { "filter suite does not exists", diff --git a/pkg/analyzer/filter/eval.go b/pkg/analyzer/filter/eval.go index b9c762d..2243592 100644 --- a/pkg/analyzer/filter/eval.go +++ b/pkg/analyzer/filter/eval.go @@ -37,12 +37,13 @@ type Evaluator interface { } type filterEvaluator struct { - name string - env *cel.Env - programs []*filterProgram + name string + env *cel.Env + programs []*filterProgram + ignoreError bool } -func NewEvaluator(name string) (Evaluator, error) { +func NewEvaluator(name string, ignoreError bool) (Evaluator, error) { env, err := cel.NewEnv( cel.Variable(filterInputVarPkg, cel.DynType), cel.Variable(filterInputVarVulns, cel.DynType), @@ -57,9 +58,10 @@ func NewEvaluator(name string) (Evaluator, error) { } return &filterEvaluator{ - name: name, - env: env, - programs: []*filterProgram{}, + name: name, + env: env, + programs: []*filterProgram{}, + ignoreError: ignoreError, }, nil } @@ -108,6 +110,12 @@ func (f *filterEvaluator) EvalPackage(pkg *models.Package) (*filterEvaluationRes }) if err != nil { + logger.Warnf("CEL evaluator error: %s", err.Error()) + + if f.ignoreError { + continue + } + return nil, err } @@ -119,7 +127,6 @@ func (f *filterEvaluator) EvalPackage(pkg *models.Package) (*filterEvaluationRes program: prog, }, nil } - } return &filterEvaluationResult{ diff --git a/pkg/analyzer/filter/result.go b/pkg/analyzer/filter/result.go index 8457d48..8db90e3 100644 --- a/pkg/analyzer/filter/result.go +++ b/pkg/analyzer/filter/result.go @@ -8,3 +8,11 @@ type filterEvaluationResult struct { func (r *filterEvaluationResult) Matched() bool { return r.match } + +func (r *filterEvaluationResult) GetMatchedFilter() *filterProgram { + if r.program == nil { + return &filterProgram{} + } + + return r.program +} diff --git a/samples/filter-suites/fs-generic.yml b/samples/filter-suites/fs-generic.yml index 956945e..a4a73fb 100644 --- a/samples/filter-suites/fs-generic.yml +++ b/samples/filter-suites/fs-generic.yml @@ -6,15 +6,17 @@ filters: - name: critical-or-high-vulns value: | vulns.critical.exists(p, true) || vulns.high.exists(p, true) - - name: ossf-best-practices + - name: low-popularity value: | - (scorecard.scores["Maintained"] == 0) || - (scorecard.scores["Binary-Artifacts"] == 0) || - (scorecard.scores["Branch-Protection"] == 0) || - (scorecard.scores["Code-Review"] == 0) || - (scorecard.scores["Dangerous-Workflow"] == 0) + 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