Skip to content

Commit

Permalink
feature: add support for allowlists
Browse files Browse the repository at this point in the history
This adds supports for denying all packages, and only allow selected ones by implementing support for `mode: "allow"`.

Such as:

```
rules:
  - pattern: "BSD-*"
    name: "bsd-allow"
  - pattern: "*"
    name: "default-deny-all"
    mode: "deny"
```

Signed-off-by: Mikail Kocak <[email protected]>
  • Loading branch information
NyanKiyoshi committed Sep 4, 2024
1 parent 4362dc2 commit 84db6a4
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 31 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ It can also be used to allow specific licenses, denying all others.
![json-output](https://github.com/anchore/grant/assets/32073428/c2d89645-e323-4f99-a179-77e5a750ee6a)

## Configuration

### Example: Deny GPL

```yaml
#.grant.yaml
config: ".grant.yaml"
Expand All @@ -109,3 +112,28 @@ rules:
exclusions:
- "alpine-base-layout" # We don't link against this package so we don't care about its license
```
### Example: Allow Lists
In this example, all licenses are denied except BSD and MIT:
```yaml
#.grant.yaml
rules:
- pattern: "BSD-*"
name: "bsd-allow"
mode: "allow"
reason: "BSD is compatible with our project"
exceptions:
# Packages to disallow even if they are licensed under BSD.
- my-package
- pattern: "MIT"
name: "mit-allow"
mode: "allow"
reason: "MIT is compatible with our project"
# Reject the rest.
- pattern: "*"
name: "default-deny-all"
mode: "deny"
reason: "All licenses need to be explicitly allowed"
```
137 changes: 137 additions & 0 deletions grant/evalutation/license_evaluation_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package evalutation

import (
"github.com/gobwas/glob"
"github.com/google/go-cmp/cmp"
"testing"

"github.com/anchore/grant/grant"
Expand Down Expand Up @@ -43,3 +45,138 @@ func Test_NewLicenseEvaluations(t *testing.T) {
func fixtureCase(fixturePath string) []grant.Case {
return grant.NewCases(fixturePath)
}

func Test_checkLicense(t *testing.T) {
tests := []struct {
name string
config EvaluationConfig
license grant.License
wants struct {
Pass bool
Reasons []Reason
}
}{
{
name: "should reject denied licenses",
license: grant.License{Name: "MIT"},
// Only allow OSI licenses.
config: EvaluationConfig{CheckNonSPDX: true, Policy: grant.DefaultPolicy()},
wants: struct {
Pass bool
Reasons []Reason
}{
Pass: false,
Reasons: []Reason{{
Detail: ReasonLicenseDeniedPolicy,
RuleName: "default-deny-all",
}},
},
},
{
name: "non-OSI approved licenses should be denied when EvaluationConfig.OsiApproved is true",
license: grant.License{
IsOsiApproved: false,
LicenseID: "AGPL-1.0-only",
SPDXExpression: "AGPL-1.0-only",
},
// Only allow OSI licenses.
config: EvaluationConfig{OsiApproved: true},
wants: struct {
Pass bool
Reasons []Reason
}{
Pass: false,
Reasons: []Reason{{
Detail: ReasonLicenseDeniedOSI,
RuleName: RuleNameNotOSIApproved,
}},
},
},
{
name: "non-OSI approved licenses should be allowed when it's not an SPDX expression",
license: grant.License{
IsOsiApproved: false,
// Non-SPDX license
Name: "AGPL-1.0-only",
},
// Only allow OSI licenses.
config: EvaluationConfig{OsiApproved: true},
wants: struct {
Pass bool
Reasons []Reason
}{
Pass: true,
Reasons: []Reason{{Detail: ReasonLicenseAllowed}},
},
},
{
name: "non-OSI approved licenses should be allowed when EvaluationConfig.OsiApproved is false",
license: grant.License{
IsOsiApproved: false,
LicenseID: "AGPL-1.0-only",
SPDXExpression: "AGPL-1.0-only",
},
config: EvaluationConfig{},
wants: struct {
Pass bool
Reasons []Reason
}{
Pass: true,
Reasons: []Reason{{Detail: ReasonLicenseAllowed}},
},
},
{
// Verifies rules are evaluated from first to last.
name: "A 'Deny' rule preceding a 'Deny' rule should always take precedence",
license: grant.License{LicenseID: "BSD-3-Clause", SPDXExpression: "BSD-3-Clause"},
config: EvaluationConfig{
Policy: grant.Policy{
Rules: []grant.Rule{
{
Name: "allow-bsd-licenses",
Glob: glob.MustCompile("bsd-*"),
Exceptions: []glob.Glob{},
Mode: grant.Allow,
Reason: "BSD licenses are allowed",
},
{
Name: "deny-all",
Glob: glob.MustCompile("*"),
Exceptions: []glob.Glob{},
Mode: grant.Deny,
Reason: "No 'Allow' rule matched, unknown licenses are not allowed.",
},
},
},
},
wants: struct {
Pass bool
Reasons []Reason
}{
Pass: true,
Reasons: []Reason{{
Detail: ReasonLicenseAllowed,
RuleName: "allow-bsd-licenses",
}},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := checkLicense(tc.config, &grant.Package{}, tc.license)
if tc.wants.Pass != result.Pass {
t.Errorf("Expected Pass to be %t, got %t", tc.wants.Pass, result.Pass)
}
if diff := cmp.Diff(tc.license, result.License); diff != "" {
t.Errorf("Mismatched 'License' field (-want +got):\n%s", diff)
}
if diff := cmp.Diff(tc.config.Policy, result.Policy); diff != "" {
t.Errorf("Mismatched 'Policy' field (-want +got):\n%s", diff)
}
if diff := cmp.Diff(tc.wants.Reasons, result.Reason); diff != "" {
t.Errorf("Mismatched 'Reasons' field (-want +got):\n%s", diff)
}
})
}
}
39 changes: 15 additions & 24 deletions grant/evalutation/license_evalutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,6 @@ func checkSBOM(ec EvaluationConfig, sb sbom.SBOM) LicenseEvaluations {
}

func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License) LicenseEvaluation {
if !l.IsSPDX() && ec.CheckNonSPDX {
if denied, rule := ec.Policy.IsDenied(l, pkg); denied {
var reason Reason
if rule != nil {
reason = Reason{
Detail: ReasonLicenseDeniedPolicy,
RuleName: rule.Name,
}
}
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{reason}, false)
}
}

if ec.OsiApproved && l.IsSPDX() {
if !l.IsOsiApproved {
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{{
Expand All @@ -65,20 +52,24 @@ func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License) Lice
}}, false)
}
}
if denied, rule := ec.Policy.IsDenied(l, pkg); denied {
var reason Reason
if rule != nil {
reason = Reason{
Detail: ReasonLicenseDeniedPolicy,
RuleName: rule.Name,
}
}
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{reason}, false)

isDenied, matchedRule := ec.Policy.IsDenied(l, pkg)

// By default, we allow unmatched rules.
detail := ReasonLicenseAllowed
ruleName := ""

if isDenied {
detail = ReasonLicenseDeniedPolicy
}
if matchedRule != nil {
ruleName = matchedRule.Name
}

return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{{
Detail: ReasonLicenseAllowed,
}}, true)
Detail: detail,
RuleName: ruleName,
}}, !isDenied)
}

type LicenseEvaluations []LicenseEvaluation
Expand Down
11 changes: 5 additions & 6 deletions grant/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,27 @@ 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, *Rule) {
for _, rule := range p.Rules {
if rule.Mode != Deny {
continue
}

var toMatch string

if license.IsSPDX() {
toMatch = strings.ToLower(license.LicenseID)
} else {
toMatch = strings.ToLower(license.Name)
}

toMatch = strings.ToLower(toMatch)
// TODO: write tests for this section
if rule.Glob.Match(toMatch) && toMatch != "" {
if pkg == nil {
return true, &rule
}
for _, exception := range rule.Exceptions {
if exception.Match(pkg.Name) {
return false, &rule
return rule.Mode != Deny, &rule
}
}
return true, &rule
// true when Mode=Deny, false otherwise
return rule.Mode == Deny, &rule
}
}
return false, nil
Expand Down
53 changes: 52 additions & 1 deletion grant/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func Test_NewPolicy(t *testing.T) {
}
}

func Test_Policy_DenyAll(t *testing.T) {
func Test_Policy_IsDenied(t *testing.T) {
tests := []struct {
name string
p Policy
Expand All @@ -101,6 +101,57 @@ func Test_Policy_DenyAll(t *testing.T) {
},
},
},

{
name: "Policy allowing all licenses",
p: Policy{
Rules: []Rule{{
Name: "allow-all",
Glob: glob.MustCompile("*"),
Exceptions: []glob.Glob{},
Mode: Allow,
Reason: "all licenses are allowed",
}},
},
want: struct {
denied bool
rule *Rule
}{
denied: false,
rule: &Rule{
Name: "allow-all",
Glob: glob.MustCompile("*"),
Exceptions: []glob.Glob{},
Mode: Allow,
Reason: "all licenses are allowed",
},
},
},
{
name: "Policy ignoring all licenses",
p: Policy{
Rules: []Rule{{
Name: "ignore-all",
Glob: glob.MustCompile("*"),
Exceptions: []glob.Glob{},
Mode: Ignore,
Reason: "all licenses are ignored",
}},
},
want: struct {
denied bool
rule *Rule
}{
denied: false,
rule: &Rule{
Name: "ignore-all",
Glob: glob.MustCompile("*"),
Exceptions: []glob.Glob{},
Mode: Ignore,
Reason: "all licenses are ignored",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
Expand Down

0 comments on commit 84db6a4

Please sign in to comment.