Skip to content

Commit 32c4643

Browse files
NyanKiyoshispiffcs
andauthored
feature: add support for allowlists (#123)
* feature: add support for allowlists 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 <mikail-gh@pm.me> * test: add nonspdx default licenses test Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * chore: add ignore as an "allow" Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * feat: update policy Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --------- Signed-off-by: Mikail Kocak <mikail-gh@pm.me> Signed-off-by: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
1 parent 7a8dc6b commit 32c4643

5 files changed

Lines changed: 264 additions & 31 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ It can also be used to allow specific licenses, denying all others.
9494
![json-output](https://github.com/anchore/grant/assets/32073428/c2d89645-e323-4f99-a179-77e5a750ee6a)
9595

9696
## Configuration
97+
98+
### Example: Deny GPL
99+
97100
```yaml
98101
#.grant.yaml
99102
config: ".grant.yaml"
@@ -109,3 +112,28 @@ rules:
109112
exclusions:
110113
- "alpine-base-layout" # We don't link against this package so we don't care about its license
111114
```
115+
116+
### Example: Allow Lists
117+
118+
In this example, all licenses are denied except BSD and MIT:
119+
120+
```yaml
121+
#.grant.yaml
122+
rules:
123+
- pattern: "BSD-*"
124+
name: "bsd-allow"
125+
mode: "allow"
126+
reason: "BSD is compatible with our project"
127+
exceptions:
128+
# Packages to disallow even if they are licensed under BSD.
129+
- my-package
130+
- pattern: "MIT"
131+
name: "mit-allow"
132+
mode: "allow"
133+
reason: "MIT is compatible with our project"
134+
# Reject the rest.
135+
- pattern: "*"
136+
name: "default-deny-all"
137+
mode: "deny"
138+
reason: "All licenses need to be explicitly allowed"
139+
```

grant/evalutation/license_evaluation_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package evalutation
22

33
import (
4+
"github.com/gobwas/glob"
5+
"github.com/google/go-cmp/cmp"
46
"testing"
57

68
"github.com/anchore/grant/grant"
@@ -43,3 +45,154 @@ func Test_NewLicenseEvaluations(t *testing.T) {
4345
func fixtureCase(fixturePath string) []grant.Case {
4446
return grant.NewCases(fixturePath)
4547
}
48+
49+
func Test_checkLicense(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
config EvaluationConfig
53+
license grant.License
54+
wants struct {
55+
Pass bool
56+
Reasons []Reason
57+
}
58+
}{
59+
{
60+
name: "should reject denied licenses when SPDX expressions and CheckNON SPDX is False",
61+
license: grant.License{ID: "MIT", SPDXExpression: "MIT", LicenseID: "MIT"},
62+
// Only allow OSI licenses.
63+
config: EvaluationConfig{CheckNonSPDX: false, Policy: grant.DefaultPolicy().SetMatchNonSPDX(false)},
64+
wants: struct {
65+
Pass bool
66+
Reasons []Reason
67+
}{
68+
Pass: false,
69+
Reasons: []Reason{{
70+
Detail: ReasonLicenseDeniedPolicy,
71+
RuleName: "default-deny-all",
72+
}},
73+
},
74+
},
75+
{
76+
name: "should reject denied licenses when CheckNonSPDX is also true",
77+
license: grant.License{Name: "foobar"},
78+
// Only allow OSI licenses.
79+
config: EvaluationConfig{CheckNonSPDX: true, Policy: grant.DefaultPolicy().SetMatchNonSPDX(true)},
80+
wants: struct {
81+
Pass bool
82+
Reasons []Reason
83+
}{
84+
Pass: false,
85+
Reasons: []Reason{{
86+
Detail: ReasonLicenseDeniedPolicy,
87+
RuleName: "default-deny-all",
88+
}},
89+
},
90+
},
91+
{
92+
name: "non-OSI approved licenses should be denied when EvaluationConfig.OsiApproved is true",
93+
license: grant.License{
94+
IsOsiApproved: false,
95+
LicenseID: "AGPL-1.0-only",
96+
SPDXExpression: "AGPL-1.0-only",
97+
},
98+
// Only allow OSI licenses.
99+
config: EvaluationConfig{OsiApproved: true},
100+
wants: struct {
101+
Pass bool
102+
Reasons []Reason
103+
}{
104+
Pass: false,
105+
Reasons: []Reason{{
106+
Detail: ReasonLicenseDeniedOSI,
107+
RuleName: RuleNameNotOSIApproved,
108+
}},
109+
},
110+
},
111+
{
112+
name: "non-OSI approved licenses should be allowed when it's not an SPDX expression",
113+
license: grant.License{
114+
IsOsiApproved: false,
115+
// Non-SPDX license
116+
Name: "AGPL-1.0-only",
117+
},
118+
// Only allow OSI licenses.
119+
config: EvaluationConfig{OsiApproved: true},
120+
wants: struct {
121+
Pass bool
122+
Reasons []Reason
123+
}{
124+
Pass: true,
125+
Reasons: []Reason{{Detail: ReasonLicenseAllowed}},
126+
},
127+
},
128+
{
129+
name: "non-OSI approved licenses should be allowed when EvaluationConfig.OsiApproved is false",
130+
license: grant.License{
131+
IsOsiApproved: false,
132+
LicenseID: "AGPL-1.0-only",
133+
SPDXExpression: "AGPL-1.0-only",
134+
},
135+
config: EvaluationConfig{},
136+
wants: struct {
137+
Pass bool
138+
Reasons []Reason
139+
}{
140+
Pass: true,
141+
Reasons: []Reason{{Detail: ReasonLicenseAllowed}},
142+
},
143+
},
144+
{
145+
// Verifies rules are evaluated from first to last.
146+
name: "A 'Deny' rule preceding a 'Deny' rule should always take precedence",
147+
license: grant.License{LicenseID: "BSD-3-Clause", SPDXExpression: "BSD-3-Clause"},
148+
config: EvaluationConfig{
149+
Policy: grant.Policy{
150+
Rules: []grant.Rule{
151+
{
152+
Name: "allow-bsd-licenses",
153+
Glob: glob.MustCompile("bsd-*"),
154+
Exceptions: []glob.Glob{},
155+
Mode: grant.Allow,
156+
Reason: "BSD licenses are allowed",
157+
},
158+
{
159+
Name: "deny-all",
160+
Glob: glob.MustCompile("*"),
161+
Exceptions: []glob.Glob{},
162+
Mode: grant.Deny,
163+
Reason: "No 'Allow' rule matched, unknown licenses are not allowed.",
164+
},
165+
},
166+
},
167+
},
168+
wants: struct {
169+
Pass bool
170+
Reasons []Reason
171+
}{
172+
Pass: true,
173+
Reasons: []Reason{{
174+
Detail: ReasonLicenseAllowed,
175+
RuleName: "allow-bsd-licenses",
176+
}},
177+
},
178+
},
179+
}
180+
181+
for _, tc := range tests {
182+
t.Run(tc.name, func(t *testing.T) {
183+
result := checkLicense(tc.config, &grant.Package{}, tc.license)
184+
if tc.wants.Pass != result.Pass {
185+
t.Errorf("Expected Pass to be %t, got %t", tc.wants.Pass, result.Pass)
186+
}
187+
if diff := cmp.Diff(tc.license, result.License); diff != "" {
188+
t.Errorf("Mismatched 'License' field (-want +got):\n%s", diff)
189+
}
190+
if diff := cmp.Diff(tc.config.Policy, result.Policy); diff != "" {
191+
t.Errorf("Mismatched 'Policy' field (-want +got):\n%s", diff)
192+
}
193+
if diff := cmp.Diff(tc.wants.Reasons, result.Reason); diff != "" {
194+
t.Errorf("Mismatched 'Reasons' field (-want +got):\n%s", diff)
195+
}
196+
})
197+
}
198+
}

grant/evalutation/license_evalutation.go

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,6 @@ func checkSBOM(ec EvaluationConfig, sb sbom.SBOM) LicenseEvaluations {
4444
}
4545

4646
func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License) LicenseEvaluation {
47-
if !l.IsSPDX() && ec.CheckNonSPDX {
48-
if denied, rule := ec.Policy.IsDenied(l, pkg); denied {
49-
var reason Reason
50-
if rule != nil {
51-
reason = Reason{
52-
Detail: ReasonLicenseDeniedPolicy,
53-
RuleName: rule.Name,
54-
}
55-
}
56-
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{reason}, false)
57-
}
58-
}
59-
6047
if ec.OsiApproved && l.IsSPDX() {
6148
if !l.IsOsiApproved {
6249
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{{
@@ -65,20 +52,24 @@ func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License) Lice
6552
}}, false)
6653
}
6754
}
68-
if denied, rule := ec.Policy.IsDenied(l, pkg); denied {
69-
var reason Reason
70-
if rule != nil {
71-
reason = Reason{
72-
Detail: ReasonLicenseDeniedPolicy,
73-
RuleName: rule.Name,
74-
}
75-
}
76-
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{reason}, false)
55+
56+
isDenied, matchedRule := ec.Policy.IsDenied(l, pkg)
57+
58+
// By default, we allow unmatched rules.
59+
detail := ReasonLicenseAllowed
60+
ruleName := ""
61+
62+
if isDenied {
63+
detail = ReasonLicenseDeniedPolicy
64+
}
65+
if matchedRule != nil {
66+
ruleName = matchedRule.Name
7767
}
7868

7969
return NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{{
80-
Detail: ReasonLicenseAllowed,
81-
}}, true)
70+
Detail: detail,
71+
RuleName: ruleName,
72+
}}, !isDenied)
8273
}
8374

8475
type LicenseEvaluations []LicenseEvaluation

grant/policy.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ func (p Policy) IsEmpty() bool {
5050
// IsDenied returns true if the given license is denied by the policy
5151
func (p Policy) IsDenied(license License, pkg *Package) (bool, *Rule) {
5252
for _, rule := range p.Rules {
53+
// ignore non spdx licenses if the rule is configured to not match on non spdx
54+
isSPDX := license.IsSPDX()
55+
matchNonSPDX := p.MatchNonSPDX
56+
if !matchNonSPDX && !isSPDX {
57+
continue
58+
}
5359
var toMatch string
5460
if license.IsSPDX() {
5561
toMatch = strings.ToLower(license.LicenseID)
@@ -62,7 +68,7 @@ func (p Policy) IsDenied(license License, pkg *Package) (bool, *Rule) {
6268
if rule.Glob.Match(toMatch) && toMatch != "" {
6369
var returnVal bool
6470
// set the return value based on the rule mode
65-
if rule.Mode == Allow {
71+
if rule.Mode == Allow || rule.Mode == Ignore {
6672
returnVal = false
6773
} else {
6874
returnVal = true
@@ -72,14 +78,18 @@ func (p Policy) IsDenied(license License, pkg *Package) (bool, *Rule) {
7278
}
7379
for _, exception := range rule.Exceptions {
7480
if exception.Match(pkg.Name) {
75-
// flip the return value based on the exception
76-
returnVal = !returnVal
77-
78-
return returnVal, &rule
81+
return rule.Mode != Deny, &rule
7982
}
8083
}
81-
return returnVal, &rule
84+
// true when Mode=Deny, false otherwise
85+
return rule.Mode == Deny, &rule
8286
}
8387
}
8488
return false, nil
8589
}
90+
91+
// SetMatchNonSPDX updates the match option for the given policy
92+
func (p Policy) SetMatchNonSPDX(matchNonSPDX bool) Policy {
93+
p.MatchNonSPDX = matchNonSPDX
94+
return p
95+
}

grant/policy_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func Test_NewPolicy(t *testing.T) {
7575
}
7676
}
7777

78-
func Test_Policy_DenyAll(t *testing.T) {
78+
func Test_Policy_IsDenied(t *testing.T) {
7979
tests := []struct {
8080
name string
8181
p Policy
@@ -101,6 +101,57 @@ func Test_Policy_DenyAll(t *testing.T) {
101101
},
102102
},
103103
},
104+
105+
{
106+
name: "Policy allowing all licenses",
107+
p: Policy{
108+
Rules: []Rule{{
109+
Name: "allow-all",
110+
Glob: glob.MustCompile("*"),
111+
Exceptions: []glob.Glob{},
112+
Mode: Allow,
113+
Reason: "all licenses are allowed",
114+
}},
115+
},
116+
want: struct {
117+
denied bool
118+
rule *Rule
119+
}{
120+
denied: false,
121+
rule: &Rule{
122+
Name: "allow-all",
123+
Glob: glob.MustCompile("*"),
124+
Exceptions: []glob.Glob{},
125+
Mode: Allow,
126+
Reason: "all licenses are allowed",
127+
},
128+
},
129+
},
130+
{
131+
name: "Policy ignoring all licenses",
132+
p: Policy{
133+
Rules: []Rule{{
134+
Name: "ignore-all",
135+
Glob: glob.MustCompile("*"),
136+
Exceptions: []glob.Glob{},
137+
Mode: Ignore,
138+
Reason: "all licenses are ignored",
139+
}},
140+
},
141+
want: struct {
142+
denied bool
143+
rule *Rule
144+
}{
145+
denied: false,
146+
rule: &Rule{
147+
Name: "ignore-all",
148+
Glob: glob.MustCompile("*"),
149+
Exceptions: []glob.Glob{},
150+
Mode: Ignore,
151+
Reason: "all licenses are ignored",
152+
},
153+
},
154+
},
104155
}
105156
for _, tc := range tests {
106157
t.Run(tc.name, func(t *testing.T) {

0 commit comments

Comments
 (0)