Skip to content

Commit

Permalink
feat: spdx license list generation (#3)
Browse files Browse the repository at this point in the history
* wip: wip

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

* feat: add code generation for spdx license list

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

* chore: update generated license list with lower case ID

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

---------

Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs authored Nov 20, 2023
1 parent 5782847 commit 37077dd
Show file tree
Hide file tree
Showing 11 changed files with 7,653 additions and 225 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ go.work.sum

# mac
.DS_Store
*.json
10 changes: 8 additions & 2 deletions .grant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
precedence: [deny, allow]
deny-licenses: "*"
allow-licenses:
- MIT
- Apache-2.0
- MPL-2.0
- BSD-2-Clause
- BSD-3-Clause
- GPL-2.0-Or-Later+
- Zlib
- MIT
- Apache-2.0

83 changes: 20 additions & 63 deletions cmd/grant/cli/command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package command
import (
"fmt"
"slices"
"strings"

"github.com/jedib0t/go-pretty/v6/list"
"github.com/jedib0t/go-pretty/v6/text"
Expand All @@ -12,7 +11,6 @@ import (

"github.com/anchore/clio"
"github.com/anchore/grant/cmd/grant/cli/option"
"github.com/anchore/grant/event"
"github.com/anchore/grant/grant"
"github.com/anchore/grant/internal/bus"
"github.com/anchore/grant/internal/input"
Expand Down Expand Up @@ -46,6 +44,8 @@ func Check(app clio.Application) *cobra.Command {
}, cfg)
}

// TODO: upgrade the ui a bit with monitors for SBOM generation and license checking
// Progress can be incremented used on a per package basis when grant.Check is called
func runCheck(cfg CheckConfig, sources []string) (errs error) {
var reports []*grant.Report
// check if user provided source by stdin
Expand All @@ -56,29 +56,7 @@ func runCheck(cfg CheckConfig, sources []string) (errs error) {
sources = append(sources, "-")
}

monitor := bus.PublishTask(
event.Title{
Default: "Check licenses from sources for non-compliance",
WhileRunning: "Checking licenses from sources for non-compliance",
OnSuccess: "Checked licenses from sources for non-compliance",
},
"",
len(sources),
)

defer func() {
if errs != nil {
monitor.SetError(errs)
} else {
monitor.AtomicStage.Set(strings.Join(sources, ", "))
monitor.SetCompleted()
}
}()

for _, src := range sources {
monitor.Increment()
monitor.AtomicStage.Set(src)

// TODO: branch into source detection here to generate the sbom
reader, err := input.GetReader(src)
if err != nil {
Expand All @@ -92,10 +70,11 @@ func runCheck(cfg CheckConfig, sources []string) (errs error) {
}

log.Debugf("found sbom format: %s, version: %s; checking licenses...", formatID, version)
report := grant.NewReport(sbom.Source.Name, cfg.Check)
report := grant.NewReport(fmt.Sprintf("%s %s", sbom.Source.Name, sbom.Source.Version), cfg.Check)
for p := range sbom.Artifacts.Packages.Enumerate() {
log.Debugf("checking package: %s for non compliant licenses...", p.Name)
report.Check(p.Name, p.Licenses)
licenses := grant.ConvertSyftLicenses(p.Licenses)
report.Check(p.Name, licenses)
}
reports = append(reports, report)
}
Expand All @@ -107,16 +86,11 @@ func runCheck(cfg CheckConfig, sources []string) (errs error) {
}

func presentReports(reports []*grant.Report) error {
if len(reports) == 0 {
bus.Report("no license compliance reports to show")
return nil
}

l := list.NewWriter()
l := list.NewWriter() // TODO: style me
customStyle := list.Style{
Format: text.FormatTitle,
CharItemSingle: "",
CharItemTop: "",
CharItemTop: "",
CharItemFirst: "",
CharItemMiddle: "",
CharItemVertical: " ",
Expand All @@ -127,41 +101,24 @@ func presentReports(reports []*grant.Report) error {
}
l.SetStyle(customStyle)
for _, report := range reports {
l.AppendItem(fmt.Sprintf("Source: %s", report.Source))
l.Indent()
for pkg, _ := range report.CheckedPackages {
l.AppendItem(fmt.Sprintf("Package: %s", pkg))
l.AppendItem("Licenses: ")
violations := report.Violations[pkg]
compliance := report.Compliant[pkg]
ignored := report.Ignored[pkg]
if len(compliance) > 0 {
l.Indent()
for _, lic := range compliance {
// green emoji check mark append
l.AppendItem(fmt.Sprintf("%s %s", text.FgGreen.Sprint("- ✅"), lic))
}
l.UnIndent()
}
if len(violations) > 0 {
l.Indent()
for _, lic := range violations {
// red emoji x mark append
l.AppendItem(fmt.Sprintf("%s %s", text.FgRed.Sprint("- ❌"), lic))
}
l.UnIndent()
}
if len(ignored) > 0 {
// grey emoji question mark append
if len(report.PackageViolations) == 0 {
l.AppendItem("No License Violations: ✅")
continue
}

l.AppendItem("License Violations:")
for license, pkg := range report.LicenseViolations {
l.AppendItem(fmt.Sprintf("%s %s", fmt.Sprint("-"), license))
// TODO: we probably want a flag that can turn this on
for _, p := range pkg {
l.Indent()
for _, lic := range ignored {
l.AppendItem(fmt.Sprintf("%s %s", text.FgHiBlack.Sprint("- ❓"), lic))
}
l.AppendItem(fmt.Sprintf("%s %s", fmt.Sprint("-"), p))
l.UnIndent()
}
l.UnIndent()
}
}

bus.Report(l.Render())
return nil
}
}
8 changes: 5 additions & 3 deletions grant/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package grant

import (
"slices"
"strings"

"github.com/anchore/grant/cmd/grant/cli/option"
)
Expand All @@ -12,18 +13,19 @@ func IsAllowed(cfg option.Check, license string) bool {

// deny licenses take precidence over allow licenses by default
// if a license is in both lists, it is denied
// if a license is in neither list, it is denied: this is the default behavior
// if a license is in neither list, it is denied; this is the default behavior
// licenses are matched on a case-insensitive basis
// TODO: add support for glob matching expressions; for now, only exact matches are supported
func isAllowed(cfg option.Check, license string) bool {
if slices.Contains(cfg.DenyLicenses, "*") {
// all licenses are denied by default
// if a license is not in the allow list, then it is a forbidden license
if slices.Contains(cfg.AllowLicenses, license) {
if slices.Contains(cfg.AllowLicenses, strings.ToLower(license)) {
return true
}
return false
}
// user has explicitly denied licenses (and no licenses are denied by default)
// user has explicitly denied licenses (no licenses are denied by default)
if slices.Contains(cfg.DenyLicenses, license) {
return false
}
Expand Down
44 changes: 44 additions & 0 deletions grant/license.go
Original file line number Diff line number Diff line change
@@ -1 +1,45 @@
package grant

import (
"github.com/anchore/syft/syft/pkg"
)

type License struct {
// SPDXExpression is the SPDX expression for the license
SPDXExpression string `json:"spdxExpression"`
// Name is the name of the individual license if SPDXExpression is unset
Name string `json:"name"`
// Value is contents of the license
Value string `json:"value"`
// Locations are the paths for a package that show evidence of the license
Locations []string `json:"location"`
}

// ConvertSyftLicenses converts a syft LicenseSet to a grant License slice
func ConvertSyftLicenses(set pkg.LicenseSet) (licenses []License) {
licenses = make([]License, 0)
for _, lic := range set.ToSlice() {
// get all the locations for the license
locations := lic.Locations.ToSlice()
licenseLocations := make([]string, 0)
for _, location := range locations {
licenseLocations = append(licenseLocations, location.RealPath)
}

if lic.SPDXExpression != "" {
licenses = append(licenses, License{
SPDXExpression: lic.SPDXExpression,
Locations: licenseLocations,
})
continue
}

// no spdx expression from syft so just add the license as-is
// currently these are ignored by the checker
licenses = append(licenses, License{
Name: lic.Value,
Locations: licenseLocations,
})
}
return licenses
}
Loading

0 comments on commit 37077dd

Please sign in to comment.