Skip to content

Commit

Permalink
feat: refine rule behavior for exceptions and config presenters (#13)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs authored Dec 11, 2023
1 parent 79083d6 commit 452dbba
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 70 deletions.
23 changes: 10 additions & 13 deletions .grant.yaml
Original file line number Diff line number Diff line change
@@ -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"
17 changes: 10 additions & 7 deletions cmd/grant/cli/command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 {
Expand All @@ -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,
})
}
Expand Down Expand Up @@ -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))
}
Expand Down
77 changes: 44 additions & 33 deletions cmd/grant/cli/internal/check/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
}
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions cmd/grant/cli/option/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
2 changes: 1 addition & 1 deletion grant/evalutation/license_evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
36 changes: 30 additions & 6 deletions grant/evalutation/license_evalutation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package evalutation

import (
"sort"

"github.com/anchore/grant/grant"
"github.com/anchore/syft/syft/sbom"
)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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] }
8 changes: 8 additions & 0 deletions grant/evalutation/reason.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package evalutation

import (
"github.com/anchore/grant/grant"
)

type Reason string

var (
ReasonNoLicenseFound Reason = "no license found"
ReasonLicenseDenied Reason = "license denied by policy"
ReasonLicenseAllowed Reason = "license allowed by policy"
)

func NewRuleReason(rule grant.Rule) Reason {
return Reason(rule.Reason)
}
1 change: 1 addition & 0 deletions grant/evalutation/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 18 additions & 10 deletions grant/policy.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package grant

import (
"strings"

"github.com/gobwas/glob"
)

Expand Down Expand Up @@ -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)
//}

0 comments on commit 452dbba

Please sign in to comment.