From 452dbba42e579134aca717b5300bc6fd83c387d3 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:00:12 -0500 Subject: [PATCH] feat: refine rule behavior for exceptions and config presenters (#13) --------- Signed-off-by: Christopher Phillips --- .grant.yaml | 23 +++--- cmd/grant/cli/command/check.go | 17 +++-- cmd/grant/cli/internal/check/report.go | 77 +++++++++++--------- cmd/grant/cli/option/rule.go | 1 + grant/evalutation/license_evaluation_test.go | 2 +- grant/evalutation/license_evalutation.go | 36 +++++++-- grant/evalutation/reason.go | 8 ++ grant/evalutation/result.go | 1 + grant/policy.go | 28 ++++--- 9 files changed, 123 insertions(+), 70 deletions(-) diff --git a/.grant.yaml b/.grant.yaml index 56fe0b1..b7e1d96 100644 --- a/.grant.yaml +++ b/.grant.yaml @@ -1,14 +1,11 @@ #.grant.yaml -format: json -import: - - ../local-policy.json - - git@githubcom:anchore/central-policy.git@main#./org/*.policy.json - - allowed: [] - denied: [] -# grant -o json alpine:latest=osi.arrpoved.json enterprisesystemsengineering:latest=special.approved.json -# grant myimage:latest ./local-policy.json - -# .gitignore vs .gitconfig distinction (don't mix the what and how) - -# .grantpolicy.yaml -# .grantpolicy/*.yaml +show-packages: true +rules: + - pattern: "*gpl*" + mode: "deny" + reason: "GPL licenses are not allowed" + exceptions: + - "lib*" + - pattern: "*gfdl*" + mode: "deny" + reason: "GPL licenses are not allowed" diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index ccb9992..f5a2bdb 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -18,14 +18,17 @@ import ( ) type CheckConfig struct { - Config string `json:"config" yaml:"config" mapstructure:"config"` - DenyRules []option.Rule `json:"deny-rules" yaml:"deny-rules" mapstructure:"deny-rules"` + Config string `json:"config" yaml:"config" mapstructure:"config"` + ShowPackages bool `json:"show-packages" yaml:"show-packages" mapstructure:"show-packages"` + Format string `json:"format" yaml:"format" mapstructure:"format"` + Rules []option.Rule `json:"rules" yaml:"rules" mapstructure:"rules"` } func DefaultCheck() *CheckConfig { return &CheckConfig{ - Config: "", - DenyRules: []option.Rule{ + Config: "", + ShowPackages: false, + Rules: []option.Rule{ { Name: "deny-all", Reason: "grant by default will deny all licenses", @@ -38,7 +41,7 @@ func DefaultCheck() *CheckConfig { func (cfg *CheckConfig) RulesFromConfig() (rules grant.Rules, err error) { rules = make(grant.Rules, 0) - for _, rule := range cfg.DenyRules { + for _, rule := range cfg.Rules { pattern := strings.ToLower(rule.Pattern) patternGlob, err := glob.Compile(pattern) if err != nil { @@ -56,7 +59,7 @@ func (cfg *CheckConfig) RulesFromConfig() (rules grant.Rules, err error) { rules = append(rules, grant.Rule{ Glob: patternGlob, Exceptions: exceptions, - Mode: grant.Deny, + Mode: grant.RuleMode(rule.Mode), Reason: rule.Reason, }) } @@ -98,7 +101,7 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { return errors.Wrap(err, fmt.Sprintf("could not check licenses; could not build policy from config: %s", cfg.Config)) } - rep, err := check.NewReport(check.Table, policy, userInput...) + rep, err := check.NewReport(check.Table, policy, cfg.ShowPackages, userInput...) if err != nil { return errors.Wrap(err, fmt.Sprintf("unable to create report for inputs %s", userInput)) } diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 084cf04..7279ffd 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -6,7 +6,6 @@ import ( "time" list "github.com/jedib0t/go-pretty/v6/list" - "github.com/jedib0t/go-pretty/v6/text" "github.com/anchore/grant/grant" "github.com/anchore/grant/grant/evalutation" @@ -33,7 +32,7 @@ type Report struct { // If no policy is provided, the default policy will be used // If no requests are provided, an empty report will be generated // If a request is provided, but the sbom cannot be generated, the source will be ignored and an error will be returned -func NewReport(f Format, rp grant.Policy, userRequests ...string) (*Report, error) { +func NewReport(f Format, rp grant.Policy, showPackages bool, userRequests ...string) (*Report, error) { if rp.IsEmpty() { rp = grant.DefaultPolicy() } @@ -48,9 +47,10 @@ func NewReport(f Format, rp grant.Policy, userRequests ...string) (*Report, erro results := evalutation.NewResults(ec, cases...) return &Report{ - Results: results, - Format: format, - Timestamp: time.Now().Format(time.RFC3339), + Results: results, + Format: format, + ShowPackages: showPackages, + Timestamp: time.Now().Format(time.RFC3339), }, nil } @@ -68,49 +68,60 @@ func (r *Report) Render(out io.Writer) error { func (r *Report) renderTable(out io.Writer) error { if !r.Results.IsFailed() { l := newList() - l.AppendItem("No License Violations: ✅") + l.AppendItem("No License Violations Found: ✅") bus.Report(l.Render()) return nil } - failures := r.Results.GetFailedEvaluations() - lists := []list.Writer{} - for input, eval := range failures { + var uiLists []list.Writer + failedEvaluations := r.Results.GetFailedEvaluations() + + // segment the results into lists by user input + // lists can optionally show the packages that were evaluated + for input, eval := range failedEvaluations { l := newList() - lists = append(lists, l) + uiLists = append(uiLists, l) + l.Indent() l.AppendItem(input) + renderLicenses(l, eval, r.ShowPackages) + } + for _, l := range uiLists { + bus.Report(l.Render()) + } + return nil +} + +func renderLicenses(l list.Writer, evals evalutation.LicenseEvaluations, showPackages bool) { + duplicates := make(map[string]struct{}) + for _, e := range evals { + var licenseRender string + if e.License.IsSPDX() { + licenseRender = e.License.SPDXExpression + } else { + licenseRender = e.License.Name + } + if _, ok := duplicates[licenseRender]; ok { + continue + } + duplicates[licenseRender] = struct{}{} l.Indent() - for _, lic := range eval.Licenses() { - if lic.IsSPDX() { - l.AppendItem(lic.LicenseID) - } else { - l.AppendItem(lic.LicenseID + " (non-SPDX)") + l.AppendItem(licenseRender) + if showPackages { + packages := evals.Packages(licenseRender) + for _, pkg := range packages { + l.Indent() + l.AppendItem(pkg) + l.UnIndent() } - } l.UnIndent() } - for _, l := range lists { - bus.Report(l.Render()) - } - return nil } func newList() list.Writer { l := list.NewWriter() - reportStyle := list.Style{ - Format: text.FormatDefault, - CharItemSingle: "▶", - CharItemTop: "-", - CharItemFirst: "-", - CharItemMiddle: "-", - CharItemBottom: "-", - CharNewline: "\n", - LinePrefix: "", - Name: "styleTest", - } - l.SetStyle(reportStyle) - + style := list.StyleDefault + style.CharItemSingle = "▶" return l } diff --git a/cmd/grant/cli/option/rule.go b/cmd/grant/cli/option/rule.go index 8f9d2ca..d0dfd9f 100644 --- a/cmd/grant/cli/option/rule.go +++ b/cmd/grant/cli/option/rule.go @@ -5,5 +5,6 @@ type Rule struct { Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` Pattern string `json:"pattern" yaml:"pattern" mapstructure:"pattern"` Severity string `json:"severity" yaml:"severity" mapstructure:"severity"` + Mode string `json:"mode" yaml:"mode" mapstructure:"mode"` Exceptions []string `json:"exceptions" yaml:"exceptions" mapstructure:"exceptions"` } diff --git a/grant/evalutation/license_evaluation_test.go b/grant/evalutation/license_evaluation_test.go index 9b9205f..94de0a4 100644 --- a/grant/evalutation/license_evaluation_test.go +++ b/grant/evalutation/license_evaluation_test.go @@ -41,5 +41,5 @@ func Test_NewLicenseEvaluations(t *testing.T) { } func fixtureCase(ec EvaluationConfig, fixturePath string) []grant.Case { - return grant.NewCases(&ec.Policy, fixturePath) + return grant.NewCases(ec.Policy, fixturePath) } diff --git a/grant/evalutation/license_evalutation.go b/grant/evalutation/license_evalutation.go index 35d5841..21600b2 100644 --- a/grant/evalutation/license_evalutation.go +++ b/grant/evalutation/license_evalutation.go @@ -1,6 +1,8 @@ package evalutation import ( + "sort" + "github.com/anchore/grant/grant" "github.com/anchore/syft/syft/sbom" ) @@ -39,7 +41,10 @@ func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License, eval if !l.IsSPDX() { // TODO: check if the config wants us to check for non-SPDX licenses } - if ec.Policy.IsDenied(l, pkg) { + if denied, rule := ec.Policy.IsDenied(l, pkg); denied { + if rule != nil { + + } le := NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{ReasonLicenseDenied}, false) return append(evaluations, le) } @@ -49,14 +54,15 @@ func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License, eval type LicenseEvaluations []LicenseEvaluation -func (le LicenseEvaluations) Packages() []grant.Package { - packages := make([]grant.Package, 0) +func (le LicenseEvaluations) Packages(license string) []string { + packages := make([]string, 0) // get the set of unique packages from the list... for _, e := range le { - if e.Package != nil { - packages = append(packages, *e.Package) + if e.Package != nil && (e.License.LicenseID == license || e.License.Name == license) { + packages = append(packages, e.Package.Name) } } + sort.Sort(sort.StringSlice(packages)) return packages } @@ -74,12 +80,13 @@ func (le LicenseEvaluations) Licenses() []grant.License { } func (le LicenseEvaluations) Failed() LicenseEvaluations { - failed := make([]LicenseEvaluation, 0) + var failed LicenseEvaluations for _, e := range le { if !e.Pass { failed = append(failed, e) } } + sort.Sort(failed) return failed } @@ -114,3 +121,20 @@ func NewLicenseEvaluation(license grant.License, pkg *grant.Package, policy gran Pass: pass, } } + +func (le LicenseEvaluations) Len() int { return len(le) } +func (le LicenseEvaluations) Less(i, j int) bool { + var compareI, compareJ string + if le[i].License.LicenseID != "" { + compareI = le[i].License.LicenseID + } else { + compareI = le[i].License.Name + } + if le[j].License.LicenseID != "" { + compareJ = le[j].License.LicenseID + } else { + compareJ = le[j].License.Name + } + return compareI < compareJ +} +func (le LicenseEvaluations) Swap(i, j int) { le[i], le[j] = le[j], le[i] } diff --git a/grant/evalutation/reason.go b/grant/evalutation/reason.go index ef04ea6..07cbb84 100644 --- a/grant/evalutation/reason.go +++ b/grant/evalutation/reason.go @@ -1,5 +1,9 @@ package evalutation +import ( + "github.com/anchore/grant/grant" +) + type Reason string var ( @@ -7,3 +11,7 @@ var ( ReasonLicenseDenied Reason = "license denied by policy" ReasonLicenseAllowed Reason = "license allowed by policy" ) + +func NewRuleReason(rule grant.Rule) Reason { + return Reason(rule.Reason) +} diff --git a/grant/evalutation/result.go b/grant/evalutation/result.go index e71ed34..732ee4d 100644 --- a/grant/evalutation/result.go +++ b/grant/evalutation/result.go @@ -37,6 +37,7 @@ func (rs Results) IsFailed() bool { return false } +// GetFailedEvaluations returns a map of user input to slice of failed license evaluations for that input func (rs Results) GetFailedEvaluations() map[string]LicenseEvaluations { failures := make(map[string]LicenseEvaluations) for _, r := range rs { diff --git a/grant/policy.go b/grant/policy.go index b899d2a..3198158 100644 --- a/grant/policy.go +++ b/grant/policy.go @@ -1,6 +1,8 @@ package grant import ( + "strings" + "github.com/gobwas/glob" ) @@ -40,28 +42,34 @@ func (p Policy) IsEmpty() bool { } // IsDenied returns true if the given license is denied by the policy -func (p Policy) IsDenied(license License, pkg *Package) bool { +func (p Policy) IsDenied(license License, pkg *Package) (bool, *Rule) { for _, rule := range p.Rules { if rule.Mode != Deny { continue } - if rule.Glob.Match(license.LicenseID) { + var toMatch string + if license.IsSPDX() { + toMatch = strings.ToLower(license.LicenseID) + } else { + toMatch = strings.ToLower(license.Name) + } + if rule.Glob.Match(toMatch) { if pkg == nil { - return true + return true, &rule } for _, exception := range rule.Exceptions { if exception.Match(pkg.Name) { - return false + return false, &rule } } - return true + return true, &rule } } - return false + return false, nil } -// IsAllowed is a convenience function for library usage of IsDenied negation -func (p Policy) IsAllowed(license License, pkg *Package) bool { - return !p.IsDenied(license, pkg) -} +//// IsAllowed is a convenience function for library usage of IsDenied negation +//func (p Policy) IsAllowed(license License, pkg *Package) bool { +// return !p.IsDenied(license, pkg) +//}