Skip to content

Commit

Permalink
feat: add rules as basic config input (#11)
Browse files Browse the repository at this point in the history
* feat: add rules as basic config input

Signed-off-by: Christopher Phillips <[email protected]>

* docs: update docs

Signed-off-by: Christopher Phillips <[email protected]>

---------

Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs authored Dec 11, 2023
1 parent d9dbd1d commit 66c33ed
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 275 deletions.
121 changes: 70 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ Manage the license compliance for oci images and software projects

![grant-demo](TODO)

### Supply an image
```bash
$ grant check alpine:latest
[return code 1]
```

#### Supply an SBOM document
```bash
$ grant check alpine.spdx.json
[return code 0]
```

### Supply multiple sources including stdin and a mix of image and sbom
```bash
$ syft -o spdx-json alpine:latest | grant check
[return code 0]
$ syft -o spdx-json alpine:latest | grant check node:latest
```


Expand All @@ -34,13 +34,22 @@ curl -sSfL https://raw.githubusercontent.com/anchore/grant/main/install.sh | sh

## Usage

Grant can be used with any OCI image or sbom document to check for license compliance.
Grant can be used with any container image, sbom document, or directory scan to check for license compliance.

Rules take the form of a pattern to match the license against, a mode to either allow or deny the license,
a reason for the rule, and a list of packages that are exclusions to the rule.
```
pattern: "gpl-*"
mode: "deny"
reason: "GPL licenses are not allowed"
exclusions:
- "alpine-base-layout" # We don't link against this package so we don't care about its license
```

Matching Rules:
- Deny licenses take precedence over allow licenses
- Licenses are matched on a case-insensitive basis.
- If a license is in both lists it is denied.
- If a license is in neither list it is denied.
- Denying licenses take precedence over allowing licenses
- License id are matched on a case-insensitive basis.
- If a license is has rules for both modes it is denied

Supplied patterns follow a standard globbing syntax:
```
Expand Down Expand Up @@ -68,59 +77,69 @@ pattern { `,` pattern }
comma-separated (without spaces) patterns
```

```bash

By default grant is configured to deny all licenses out of the box.


Grant can be used to deny specific licenses, allowing all others.
It can also be used to allow specific licenses, denying all others.

The following is an example of a `deny` oriented configuration which will deny `*` and allow `MIT` and `Apache-2`:

```yaml
#.grant.yaml
deny: *
allow:
- MIT
- Apache-*
```

If licenses are found that are not in the allow list, grant will return status code 1.

Valid IDs that are considered are by default sourced from the most recent
[SPDX license list](https://spdx.org/licenses/).

## TODO
In the future it will be possible for users to configure which license list
they would like to use for their `grant check` run.

## Output
```json
{
"result": "fail",
"reasons": [
{
"reason": "'GPL-1' is not allowed with rule 'GPL*'",
"package": {
"name": "my-pkg",
"version": "v1.0.0",
"location": "/etc/my-pkg"
}
}
]
}
#### Table
```bash
$ grant check ubuntu:latest, alpine:latest
▶ ubuntu:latest
- GPL-2.0-only
- GPL-3.0-only
- BSD-2-Clause
- BSD-3-Clause
- BSD-4-Clause
- GPL-2.0-or-later
- GPL-3.0-or-later
- LGPL-2.0-only
- LGPL-2.0-or-later
- LGPL-2.1-only
- LGPL-2.1-or-later
- LGPL-3.0-only
- LGPL-3.0-or-later
- MIT
- FSFUL
- FSFULLR
- GFDL-1.3-only
- GFDL-1.2-only
- CC0-1.0
- GPL-1.0-only
- Apache-2.0
- X11
- ISC
- GPL-1.0-or-later
- GFDL-1.2-or-later
- Zlib
- Artistic-2.0
▶ alpine:latest
- GPL-2.0-only
- MIT
- MPL-2.0
- BSD-2-Clause
- BSD-3-Clause
- Apache-2.0
- GPL-2.0-or-later
- Zlib
[return code 1]
````

#### JSON: TODO
```
```
## Configuration
```yaml
#.grant.yaml
precedence: [deny, allow]
deny: *
allow:
- MIT
- Apache-2
config: ".grant.yaml"
quite: false # only print status code 1 or 0 for success or failure
rules:
- pattern: "gpl-*"
mode: "deny"
reason: "GPL licenses are not allowed"
exclusions:
- "alpine-base-layout" # We don't link against this package so we don't care about its license
```
70 changes: 53 additions & 17 deletions cmd/grant/cli/command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"os"
"slices"
"strings"

"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/spf13/cobra"

Expand All @@ -16,17 +18,53 @@ import (
)

type CheckConfig struct {
Config string `json:"config" yaml:"config" mapstructure:"config"`
Format string `json:"format" yaml:"format" mapstructure:"format"`
option.Check `json:"" yaml:",inline" mapstructure:",squash"`
Config string `json:"config" yaml:"config" mapstructure:"config"`
DenyRules []option.Rule `json:"deny-rules" yaml:"deny-rules" mapstructure:"deny-rules"`
}

func Check(app clio.Application) *cobra.Command {
cfg := &CheckConfig{
Check: option.DefaultCheck(),
Format: string(check.Table),
func DefaultCheck() *CheckConfig {
return &CheckConfig{
Config: "",
DenyRules: []option.Rule{
{
Name: "deny-all",
Reason: "grant by default will deny all licenses",
Pattern: "*",
Severity: "high",
},
},
}
}

func (cfg *CheckConfig) RulesFromConfig() (rules grant.Rules, err error) {
rules = make(grant.Rules, 0)
for _, rule := range cfg.DenyRules {
pattern := strings.ToLower(rule.Pattern)
patternGlob, err := glob.Compile(pattern)
if err != nil {
return rules, err
}
exceptions := make([]glob.Glob, 0)
for _, exception := range rule.Exceptions {
exception = strings.ToLower(exception)
exceptionGlob, err := glob.Compile(exception)
if err != nil {
return rules, err
}
exceptions = append(exceptions, exceptionGlob)
}
rules = append(rules, grant.Rule{
Glob: patternGlob,
Exceptions: exceptions,
Mode: grant.Deny,
Reason: rule.Reason,
})
}
return rules, nil
}

func Check(app clio.Application) *cobra.Command {
cfg := DefaultCheck()
// sources are the oci images, sboms, or directories/files to check
var sources []string
return app.SetupCommand(&cobra.Command{
Expand All @@ -38,31 +76,29 @@ func Check(app clio.Application) *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runCheck(*cfg, sources)
return runCheck(cfg, sources)
},
}, cfg)
}

func runCheck(cfg CheckConfig, userInput []string) (errs error) {
func runCheck(cfg *CheckConfig, userInput []string) (errs error) {
// check if user provided source by stdin
// note: cat sbom.json | grant check spdx.json - is supported
// it will generate results for both stdin and spdx.json
isStdin, _ := input.IsStdinPipeOrRedirect()
if isStdin && !slices.Contains(userInput, "-") {
userInput = append(userInput, "-")
}

policy, err := grant.NewPolicy(cfg.AllowLicenses, cfg.DenyLicenses, cfg.IgnoreLicenses)
rules, err := cfg.RulesFromConfig()
if err != nil {
return errors.Wrap(err, fmt.Sprintf("could not check licenses; could not build policy from config: %s", cfg.Config))
return errors.Wrap(err, fmt.Sprintf("could not check licenses; could not build rules from config: %s", cfg.Config))
}

// TODO: we need to support the ability to write the report to a file without redirecting stdout
checkConfig := check.Config{
Policy: policy,
policy, err := grant.NewPolicy(rules...)
if err != nil {
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, checkConfig, userInput...)
rep, err := check.NewReport(check.Table, policy, userInput...)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to create report for inputs %s", userInput))
}
Expand Down
Loading

0 comments on commit 66c33ed

Please sign in to comment.