From 6f4ed8233f5f9b06299ab192c7038f86cef79bd9 Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Tue, 23 Jan 2024 11:55:27 -0500 Subject: [PATCH 1/5] chore: refactor policy outside of case Signed-off-by: Christopher Phillips --- README.md | 2 +- cmd/grant/cli/command/check.go | 3 +- cmd/grant/cli/command/list.go | 3 +- cmd/grant/cli/internal/check/report.go | 62 ++++++++++---------- cmd/grant/cli/internal/config.go | 8 +++ cmd/grant/cli/internal/{check => }/format.go | 4 +- cmd/grant/cli/internal/list/report.go | 49 ++++++++++++++++ fixtures/multiple/gpl | 2 +- grant/case.go | 21 +++---- 9 files changed, 103 insertions(+), 51 deletions(-) create mode 100644 cmd/grant/cli/internal/config.go rename cmd/grant/cli/internal/{check => }/format.go (82%) create mode 100644 cmd/grant/cli/internal/list/report.go diff --git a/README.md b/README.md index 39764f8..8831521 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ curl -sSfL https://raw.githubusercontent.com/anchore/grant/main/install.sh | sh ## Usage -Grant can be used with any container image, sbom document, or directory to scan for licenses and check those results +Grant can be used with any container image, sbom document, or directory to scan for licenses and check those classifierResults against a set of rules provided by the user. Rules take the form of a pattern to match the license against, a name to identify the rule, a mode to either allow, diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index 56999ac..6a45528 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/anchore/clio" + "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/cmd/grant/cli/internal/check" "github.com/anchore/grant/cmd/grant/cli/option" "github.com/anchore/grant/event" @@ -115,7 +116,7 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { reportConfig := check.ReportConfig{ Policy: policy, - Format: check.Format(cfg.Output), + Format: internal.Format(cfg.Output), ShowPackages: cfg.ShowPackages, CheckNonSPDX: cfg.CheckNonSPDX, OsiApproved: cfg.OsiApproved, diff --git a/cmd/grant/cli/command/list.go b/cmd/grant/cli/command/list.go index bd4fc5c..08de64c 100644 --- a/cmd/grant/cli/command/list.go +++ b/cmd/grant/cli/command/list.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/anchore/clio" + "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/cmd/grant/cli/internal/check" "github.com/anchore/grant/cmd/grant/cli/option" "github.com/anchore/grant/event" @@ -70,7 +71,7 @@ func runList(cfg *ListConfig, userInput []string) (errs error) { }() reportConfig := check.ReportConfig{ - Format: check.Format(cfg.Output), + Format: internal.Format(cfg.Output), ShowPackages: cfg.ShowPackages, CheckNonSPDX: cfg.CheckNonSPDX, Policy: grant.DefaultPolicy(), diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 4819de5..90f78b2 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -9,6 +9,7 @@ import ( "github.com/gookit/color" list "github.com/jedib0t/go-pretty/v6/list" + "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/event" "github.com/anchore/grant/grant" "github.com/anchore/grant/grant/evalutation" @@ -31,12 +32,9 @@ type Report struct { } type ReportConfig struct { - Format Format - Policy grant.Policy - ShowPackages bool - CheckNonSPDX bool - OsiApproved bool - Monitor *event.ManualStagedProgress + Policy grant.Policy + Options internal.ReportOptions + Monitor *event.ManualStagedProgress } // NewReport will generate a new report for the given format. @@ -50,12 +48,12 @@ func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { rc.Policy = grant.DefaultPolicy() } - rc.Format = validateFormat(rc.Format) + rc.Options.Format = internal.ValidateFormat(rc.Options.Format) cases := grant.NewCases(rc.Policy, userRequests...) ec := evalutation.EvaluationConfig{ Policy: rc.Policy, - CheckNonSPDX: rc.CheckNonSPDX, - OsiApproved: rc.OsiApproved, + CheckNonSPDX: rc.Options.CheckNonSPDX, + OsiApproved: rc.Options.OsiApproved, } results := evalutation.NewResults(ec, cases...) @@ -70,33 +68,33 @@ func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { // Render will call Render on each result in the report and return the report func (r *Report) Render() error { - switch r.Config.Format { - case Table: + switch r.Config.Options.Format { + case internal.Table: return r.renderCheckTree() - case JSON: + case internal.JSON: return r.renderJSON() } return errors.Join(r.errors...) } func (r *Report) RenderList() error { - switch r.Config.Format { - case Table: + switch r.Config.Options.Format { + case internal.Table: return r.renderList() - case JSON: + case internal.JSON: return errors.New("json format not yet supported") } return errors.Join(r.errors...) } -type GrantReport struct { - ReportID string `json:"report_id" yaml:"report_id"` - Timestamp string `json:"timestamp" yaml:"timestamp"` - Inputs []string `json:"inputs" yaml:"inputs"` - Results []ReportEvaluation `json:"results" yaml:"results"` +type Response struct { + ReportID string `json:"report_id" yaml:"report_id"` + Timestamp string `json:"timestamp" yaml:"timestamp"` + Inputs []string `json:"inputs" yaml:"inputs"` + Results []Evaluation `json:"results" yaml:"results"` } -type ReportEvaluation struct { +type Evaluation struct { Input string `json:"input" yaml:"input"` License string `json:"license" yaml:"license"` Package string `json:"package" yaml:"package"` @@ -104,7 +102,7 @@ type ReportEvaluation struct { Reasons []string `json:"reasons" yaml:"reasons"` } -func NewReportEvaluation(input string, le evalutation.LicenseEvaluation) ReportEvaluation { +func NewEvaluation(input string, le evalutation.LicenseEvaluation) Evaluation { licenseName := le.License.SPDXExpression if !le.License.IsSPDX() { licenseName = le.License.Name @@ -115,7 +113,7 @@ func NewReportEvaluation(input string, le evalutation.LicenseEvaluation) ReportE details := r.Detail reasons = append(reasons, details) } - re := ReportEvaluation{ + re := Evaluation{ Input: input, License: licenseName, Package: le.Package.Name, @@ -126,14 +124,14 @@ func NewReportEvaluation(input string, le evalutation.LicenseEvaluation) ReportE } func (r *Report) renderJSON() error { - evaluations := make([]ReportEvaluation, 0) + evaluations := make([]Evaluation, 0) for _, res := range r.Results { for _, e := range res.Evaluations { - re := NewReportEvaluation(res.Case.UserInput, e) + re := NewEvaluation(res.Case.UserInput, e) evaluations = append(evaluations, re) } } - report := GrantReport{ + report := Response{ ReportID: r.ReportID, Timestamp: r.Timestamp, Inputs: r.Results.UserInputs(), @@ -165,9 +163,9 @@ func (r *Report) renderCheckTree() error { resulList.UnIndent() continue } - renderEvaluations(rule, r.Config.ShowPackages, resulList, failedEvaluations) + renderEvaluations(rule, r.Config.Options.ShowPackages, resulList, failedEvaluations) } - if r.Config.OsiApproved { + if r.Config.Options.OsiApproved { osiRule := grant.Rule{ Name: evalutation.RuleNameNotOSIApproved, } @@ -178,10 +176,10 @@ func (r *Report) renderCheckTree() error { resulList.AppendItem(color.Success.Sprintf("%s", "No OSI Violations Found")) resulList.UnIndent() } else { - renderEvaluations(osiRule, r.Config.ShowPackages, resulList, failedEvaluations) + renderEvaluations(osiRule, r.Config.Options.ShowPackages, resulList, failedEvaluations) } } - if r.Config.ShowPackages { + if r.Config.Options.ShowPackages { renderOrphanPackages(resulList, res, false) // keep primary coloring for tree } } @@ -207,7 +205,7 @@ func (r *Report) renderList() error { resulList.Indent() resulList.AppendItem(color.Light.Sprintf("%s", license)) resulList.UnIndent() - if r.Config.ShowPackages { + if r.Config.Options.ShowPackages { packages := res.Evaluations.Packages(license) resulList.Indent() resulList.Indent() @@ -218,7 +216,7 @@ func (r *Report) renderList() error { resulList.UnIndent() } } - if r.Config.ShowPackages { + if r.Config.Options.ShowPackages { renderOrphanPackages(resulList, res, true) } } diff --git a/cmd/grant/cli/internal/config.go b/cmd/grant/cli/internal/config.go new file mode 100644 index 0000000..2b39173 --- /dev/null +++ b/cmd/grant/cli/internal/config.go @@ -0,0 +1,8 @@ +package internal + +type ReportOptions struct { + Format Format + ShowPackages bool + CheckNonSPDX bool + OsiApproved bool +} diff --git a/cmd/grant/cli/internal/check/format.go b/cmd/grant/cli/internal/format.go similarity index 82% rename from cmd/grant/cli/internal/check/format.go rename to cmd/grant/cli/internal/format.go index 6fea576..337fc90 100644 --- a/cmd/grant/cli/internal/check/format.go +++ b/cmd/grant/cli/internal/format.go @@ -1,4 +1,4 @@ -package check +package internal type Format string @@ -8,7 +8,7 @@ const ( ) // validFormat returns a valid format or the default format if the given format is invalid -func validateFormat(f Format) Format { +func ValidateFormat(f Format) Format { switch f { case "json": return JSON diff --git a/cmd/grant/cli/internal/list/report.go b/cmd/grant/cli/internal/list/report.go new file mode 100644 index 0000000..eb0563e --- /dev/null +++ b/cmd/grant/cli/internal/list/report.go @@ -0,0 +1,49 @@ +package list + +import ( + "time" + + "github.com/anchore/grant/cmd/grant/cli/internal" + "github.com/anchore/grant/event" + "github.com/anchore/grant/grant" + "github.com/anchore/grant/grant/evalutation" +) + +type Report struct { + ReportID string + Results evalutation.Results + Config ReportConfig + Timestamp string + Monitor *event.ManualStagedProgress + errors []error +} + +type ReportConfig struct { + Options internal.ReportOptions + Monitor *event.ManualStagedProgress +} + +// NewReport will generate a new report for the given format. +// The supplied policy is applied to all user requests. +// 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 +// Where do we render packages that had no licenses? +func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { + rc.Options.Format = internal.ValidateFormat(rc.Options.Format) + // TODO: we need a builder here that generates cases before the policy is applied + cases := grant.NewCases(userRequests...) + ec := evalutation.EvaluationConfig{ + CheckNonSPDX: rc.Options.CheckNonSPDX, + OsiApproved: rc.Options.OsiApproved, + } + + results := evalutation.NewResults(ec, cases...) + + return &Report{ + Results: results, + Config: rc, + Timestamp: time.Now().Format(time.RFC3339), + Monitor: rc.Monitor, + }, nil +} diff --git a/fixtures/multiple/gpl b/fixtures/multiple/gpl index 3ab3340..0289186 100644 --- a/fixtures/multiple/gpl +++ b/fixtures/multiple/gpl @@ -129,7 +129,7 @@ Termination of your rights under this section does not terminate the licenses of You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. -An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work classifierResults from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. diff --git a/grant/case.go b/grant/case.go index 1c29340..de3e5fe 100644 --- a/grant/case.go +++ b/grant/case.go @@ -24,8 +24,7 @@ import ( "github.com/anchore/syft/syft/source" ) -// Case is a collection of SBOMs and Licenses that are evaluated against a policy - +// Case is a collection of SBOMs and Licenses that are evaluated for a given UserInput type Case struct { // SBOMS is a list of SBOMs that have licenses checked against the policy SBOMS []sbom.SBOM @@ -35,12 +34,9 @@ type Case struct { // UserInput is the string that was supplied by the user to build the case UserInput string - - // Policy is the policy that is evaluated against the case - Policy Policy } -func NewCases(p Policy, userInputs ...string) []Case { +func NewCases(userInputs ...string) []Case { cases := make([]Case, 0) ch, err := NewCaseHandler() if err != nil { @@ -54,7 +50,6 @@ func NewCases(p Policy, userInputs ...string) []Case { log.Errorf("unable to determine case for %s: %+v", userInput, err) continue } - c.Policy = p c.UserInput = userInput cases = append(cases, c) } @@ -153,13 +148,13 @@ func (ch *CaseHandler) handleFile(path string) (c Case, err error) { } // let's see if it's an SBOM - bytes, err := getReadSeeker(path) + sbomBytes, err := getReadSeeker(path) if err != nil { // We bail here since we can't get a reader for the file return c, err } - sb, _, _, err := format.NewDecoderCollection(format.Decoders()...).Decode(bytes) + sb, _, _, err := format.NewDecoderCollection(format.Decoders()...).Decode(sbomBytes) if err != nil { log.Debugf("unable to determine SBOM or licenses for %s: %+v", path, err) // we want to log the error, but we don't want to return yet @@ -203,12 +198,12 @@ func (ch *CaseHandler) handleLicenseFile(path string) ([]License, error) { // re-enable logging for the rest of the application golog.SetOutput(os.Stdout) - results := ch.Backend.GetResults() - if len(results) == 0 { - return nil, fmt.Errorf("no results from license classifier") + classifierResults := ch.Backend.GetResults() + if len(classifierResults) == 0 { + return nil, fmt.Errorf("no classifierResults from license classifier") } - licenses := grantLicenseFromClassifierResults(results) + licenses := grantLicenseFromClassifierResults(classifierResults) return licenses, nil } From 33548f1b16eb4d88e42de992729852500633e594 Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Wed, 24 Jan 2024 11:16:19 -0500 Subject: [PATCH 2/5] feat: update cli options to be separate per commands Signed-off-by: Christopher Phillips --- cmd/grant/cli/command/check.go | 14 ++- cmd/grant/cli/command/list.go | 20 ++-- cmd/grant/cli/internal/check/report.go | 21 ++-- cmd/grant/cli/internal/format.go | 10 +- cmd/grant/cli/internal/list/report.go | 86 ++++++++++++-- go.mod | 2 +- grant/case.go | 27 ++++- grant/evalutation/license_evaluation_test.go | 6 +- grant/evalutation/license_evalutation.go | 2 +- grant/evalutation/syft.go | 111 ------------------- grant/license.go | 94 ++++++++++++++++ grant/package.go | 17 +++ 12 files changed, 249 insertions(+), 161 deletions(-) delete mode 100644 grant/evalutation/syft.go diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index 6a45528..7c97271 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -115,12 +115,14 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { } reportConfig := check.ReportConfig{ - Policy: policy, - Format: internal.Format(cfg.Output), - ShowPackages: cfg.ShowPackages, - CheckNonSPDX: cfg.CheckNonSPDX, - OsiApproved: cfg.OsiApproved, - Monitor: monitor, + Policy: policy, + Options: internal.ReportOptions{ + Format: internal.Format(cfg.Output), + ShowPackages: cfg.ShowPackages, + CheckNonSPDX: cfg.CheckNonSPDX, + OsiApproved: cfg.OsiApproved, + }, + Monitor: monitor, } rep, err := check.NewReport(reportConfig, userInput...) if err != nil { diff --git a/cmd/grant/cli/command/list.go b/cmd/grant/cli/command/list.go index 08de64c..dcc1858 100644 --- a/cmd/grant/cli/command/list.go +++ b/cmd/grant/cli/command/list.go @@ -8,10 +8,9 @@ import ( "github.com/anchore/clio" "github.com/anchore/grant/cmd/grant/cli/internal" - "github.com/anchore/grant/cmd/grant/cli/internal/check" + "github.com/anchore/grant/cmd/grant/cli/internal/list" "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" ) @@ -70,16 +69,17 @@ func runList(cfg *ListConfig, userInput []string) (errs error) { } }() - reportConfig := check.ReportConfig{ - Format: internal.Format(cfg.Output), - ShowPackages: cfg.ShowPackages, - CheckNonSPDX: cfg.CheckNonSPDX, - Policy: grant.DefaultPolicy(), - Monitor: monitor, + reportConfig := list.ReportConfig{ + Options: internal.ReportOptions{ + Format: internal.Format(cfg.Output), + ShowPackages: cfg.ShowPackages, + CheckNonSPDX: cfg.CheckNonSPDX, + }, + Monitor: monitor, } - rep, err := check.NewReport(reportConfig, userInput...) + rep, err := list.NewReport(reportConfig, userInput...) if err != nil { return err } - return rep.RenderList() + return rep.Render() } diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 90f78b2..604c91e 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -3,6 +3,7 @@ package check import ( "encoding/json" "errors" + "fmt" "strings" "time" @@ -37,7 +38,7 @@ type ReportConfig struct { Monitor *event.ManualStagedProgress } -// NewReport will generate a new report for the given format. +// NewReport will generate a new report for the given format for the check command // The supplied policy is applied to all user requests. // If no policy is provided, the default policy will be used // If no requests are provided, an empty report will be generated @@ -49,7 +50,7 @@ func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { } rc.Options.Format = internal.ValidateFormat(rc.Options.Format) - cases := grant.NewCases(rc.Policy, userRequests...) + cases := grant.NewCases(userRequests...) ec := evalutation.EvaluationConfig{ Policy: rc.Policy, CheckNonSPDX: rc.Options.CheckNonSPDX, @@ -73,18 +74,10 @@ func (r *Report) Render() error { return r.renderCheckTree() case internal.JSON: return r.renderJSON() + default: + r.errors = append(r.errors, fmt.Errorf("invalid format: %s; valid formats are: %s", r.Config.Options.Format, internal.ValidFormats)) + return errors.Join(r.errors...) } - return errors.Join(r.errors...) -} - -func (r *Report) RenderList() error { - switch r.Config.Options.Format { - case internal.Table: - return r.renderList() - case internal.JSON: - return errors.New("json format not yet supported") - } - return errors.Join(r.errors...) } type Response struct { @@ -155,7 +148,7 @@ func (r *Report) renderCheckTree() error { uiLists = append(uiLists, resulList) resulList.AppendItem(color.Primary.Sprintf("%s", res.Case.UserInput)) - for _, rule := range res.Case.Policy.Rules { + for _, rule := range r.Config.Policy.Rules { failedEvaluations := r.Results.GetFailedEvaluations(res.Case.UserInput, rule) if len(failedEvaluations) == 0 { resulList.Indent() diff --git a/cmd/grant/cli/internal/format.go b/cmd/grant/cli/internal/format.go index 337fc90..96b3b3d 100644 --- a/cmd/grant/cli/internal/format.go +++ b/cmd/grant/cli/internal/format.go @@ -1,5 +1,7 @@ package internal +import "github.com/google/uuid" + type Format string const ( @@ -7,7 +9,9 @@ const ( Table Format = "table" ) -// validFormat returns a valid format or the default format if the given format is invalid +var ValidFormats = []Format{JSON, Table} + +// ValidateFormat returns a valid format or the default format if the given format is invalid func ValidateFormat(f Format) Format { switch f { case "json": @@ -18,3 +22,7 @@ func ValidateFormat(f Format) Format { return Table } } + +func NewReportID() string { + return uuid.Must(uuid.NewRandom()).String() +} diff --git a/cmd/grant/cli/internal/list/report.go b/cmd/grant/cli/internal/list/report.go index eb0563e..1fe7827 100644 --- a/cmd/grant/cli/internal/list/report.go +++ b/cmd/grant/cli/internal/list/report.go @@ -1,17 +1,20 @@ package list import ( + "encoding/json" + "errors" + "fmt" "time" "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/event" "github.com/anchore/grant/grant" - "github.com/anchore/grant/grant/evalutation" + "github.com/anchore/grant/internal/bus" ) type Report struct { ReportID string - Results evalutation.Results + Cases []grant.Case Config ReportConfig Timestamp string Monitor *event.ManualStagedProgress @@ -23,7 +26,7 @@ type ReportConfig struct { Monitor *event.ManualStagedProgress } -// NewReport will generate a new report for the given format. +// NewReport will generate a new report for the given format for the list command. // The supplied policy is applied to all user requests. // If no policy is provided, the default policy will be used // If no requests are provided, an empty report will be generated @@ -31,19 +34,80 @@ type ReportConfig struct { // Where do we render packages that had no licenses? func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { rc.Options.Format = internal.ValidateFormat(rc.Options.Format) - // TODO: we need a builder here that generates cases before the policy is applied cases := grant.NewCases(userRequests...) - ec := evalutation.EvaluationConfig{ - CheckNonSPDX: rc.Options.CheckNonSPDX, - OsiApproved: rc.Options.OsiApproved, - } - - results := evalutation.NewResults(ec, cases...) return &Report{ - Results: results, + ReportID: internal.NewReportID(), + Cases: cases, Config: rc, Timestamp: time.Now().Format(time.RFC3339), Monitor: rc.Monitor, }, nil } + +func (r *Report) Render() error { + switch r.Config.Options.Format { + case internal.Table: + return r.renderList() + case internal.JSON: + return r.renderJSON() + default: + r.errors = append(r.errors, fmt.Errorf("invalid format: %s; valid formats are: %s", r.Config.Options.Format, internal.ValidFormats)) + return errors.Join(r.errors...) + } +} + +type Response struct { + ReportID string `json:"report_id" yaml:"report_id"` + Timestamp string `json:"timestamp" yaml:"timestamp"` + Inputs []string `json:"inputs" yaml:"inputs"` + Results []Result `json:"results" yaml:"results"` +} + +type Result struct { + Input string `json:"input" yaml:"input"` + License string `json:"license" yaml:"license"` + Package string `json:"package" yaml:"package"` +} + +func NewResult(input, license, lp string) Result { + return Result{ + Input: input, + License: license, + Package: lp, + } +} + +func (r *Report) renderJSON() error { + resp := Response{ + ReportID: r.ReportID, + Timestamp: r.Timestamp, + Inputs: make([]string, 0), + Results: make([]Result, 0), + } + + for _, c := range r.Cases { + resp.Inputs = append(resp.Inputs, c.UserInput) + licenses := c.GetLicenses() + for _, l := range licenses { + resp.Results = append(resp.Results, NewResult(c.UserInput, l.Name, "")) + } + } + jsonData, err := json.Marshal(resp) + if err != nil { + return err + } + + bus.Report(string(jsonData)) + return nil +} + +func (r *Report) renderList() error { + _ = Response{ + ReportID: r.ReportID, + Timestamp: r.Timestamp, + Inputs: make([]string, 0), + Results: make([]Result, 0), + } + return nil +} diff --git a/go.mod b/go.mod index 8ceec2e..8ccb0b2 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gobwas/glob v0.2.3 github.com/google/go-cmp v0.6.0 github.com/google/licenseclassifier/v2 v2.0.0 + github.com/google/uuid v1.4.0 github.com/gookit/color v1.5.4 github.com/hashicorp/go-multierror v1.1.1 github.com/jedib0t/go-pretty/v6 v6.4.9 @@ -96,7 +97,6 @@ require ( github.com/google/go-containerregistry v0.16.1 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect - github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect diff --git a/grant/case.go b/grant/case.go index de3e5fe..544504c 100644 --- a/grant/case.go +++ b/grant/case.go @@ -26,10 +26,10 @@ import ( // Case is a collection of SBOMs and Licenses that are evaluated for a given UserInput type Case struct { - // SBOMS is a list of SBOMs that have licenses checked against the policy + // SBOMS is a list of SBOMs that were generated for the user input SBOMS []sbom.SBOM - // Licenses is a list of licenses that are checked against the policy + // Licenses is a list of licenses that were generated for the user input Licenses []License // UserInput is the string that was supplied by the user to build the case @@ -56,6 +56,28 @@ func NewCases(userInputs ...string) []Case { return cases } +func (c Case) GetLicenses() []License { + licenses := make([]License, 0) + for _, sb := range c.SBOMS { + for pkg := range sb.Artifacts.Packages.Enumerate() { + grantPkg := ConvertSyftPackage(pkg) + if len(grantPkg.Licenses) == 0 { + continue + } + + for _, l := range grantPkg.Licenses { + licenses = append(licenses, l) + } + } + } + + for _, l := range c.Licenses { + licenses = append(licenses, l) + } + + return licenses +} + type CaseHandler struct { Backend *backend.ClassifierBackend } @@ -179,7 +201,6 @@ func (ch *CaseHandler) handleFile(path string) (c Case, err error) { func (ch *CaseHandler) handleLicenseFile(path string) ([]License, error) { // alright we couldn't get an SBOM, let's see if the bytes are just a LICENSE (google license classifier) - // TODO: this is a little heavy, we might want to generate a backend and reuse it for all the files we're checking // google license classifier is noisy, so we'll silence it for now golog.SetOutput(io.Discard) diff --git a/grant/evalutation/license_evaluation_test.go b/grant/evalutation/license_evaluation_test.go index f63fe0f..08b60c2 100644 --- a/grant/evalutation/license_evaluation_test.go +++ b/grant/evalutation/license_evaluation_test.go @@ -23,7 +23,7 @@ func Test_NewLicenseEvaluations(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - grantCases := fixtureCase(tc.config, tc.caseFixture) + grantCases := fixtureCase(tc.caseFixture) for _, c := range grantCases { caseEvaluations := NewLicenseEvaluations(tc.config, c) if len(caseEvaluations) == 0 { @@ -40,6 +40,6 @@ func Test_NewLicenseEvaluations(t *testing.T) { } } -func fixtureCase(ec EvaluationConfig, fixturePath string) []grant.Case { - return grant.NewCases(ec.Policy, fixturePath) +func fixtureCase(fixturePath string) []grant.Case { + return grant.NewCases(fixturePath) } diff --git a/grant/evalutation/license_evalutation.go b/grant/evalutation/license_evalutation.go index 314198f..2504242 100644 --- a/grant/evalutation/license_evalutation.go +++ b/grant/evalutation/license_evalutation.go @@ -25,7 +25,7 @@ func checkSBOM(ec EvaluationConfig, sb sbom.SBOM) LicenseEvaluations { evaluations := make([]LicenseEvaluation, 0) for pkg := range sb.Artifacts.Packages.Enumerate() { // since we use syft as a library to generate the sbom we need to convert its packages/licenses to grant types - grantPkg := convertSyftPackage(pkg) + grantPkg := grant.ConvertSyftPackage(pkg) if len(grantPkg.Licenses) == 0 { // We need to include an evaluation that shows this package has no licenses le := NewLicenseEvaluation(grant.License{}, grantPkg, ec.Policy, []Reason{{ diff --git a/grant/evalutation/syft.go b/grant/evalutation/syft.go deleted file mode 100644 index 7df41e3..0000000 --- a/grant/evalutation/syft.go +++ /dev/null @@ -1,111 +0,0 @@ -package evalutation - -import ( - "strings" - - "github.com/github/go-spdx/v2/spdxexp" - - "github.com/anchore/grant/grant" - "github.com/anchore/grant/internal/log" - "github.com/anchore/grant/internal/spdxlicense" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -func convertSyftPackage(p syftPkg.Package) *grant.Package { - locations := p.Locations.ToSlice() - packageLocations := make([]string, 0) - for _, location := range locations { - packageLocations = append(packageLocations, location.RealPath) - } - - return &grant.Package{ - Name: p.Name, - Version: p.Version, - Licenses: convertSyftLicenses(p.Licenses), - Locations: packageLocations, - } -} - -// convertSyftLicenses converts a syft LicenseSet to a grant License slice -// note: syft licenses can sometimes have complex SPDX expressions. -// Grant licenses break down these expressions into individual licenses. -// Because license expressions could potentially contain multiple licenses -// that are already represented in the syft license set we need to de-duplicate -// syft licenses have a "Value" field which is the name of the license -// given to an invalid SPDX expression; grant licenses store this field as "Name" -func convertSyftLicenses(set syftPkg.LicenseSet) (licenses []grant.License) { - licenses = make([]grant.License, 0) - checked := make(map[string]bool) - for _, license := range set.ToSlice() { - locations := license.Locations.ToSlice() - licenseLocations := make([]string, 0) - for _, location := range locations { - licenseLocations = append(licenseLocations, location.RealPath) - } - - if license.SPDXExpression != "" { - licenses = handleSPDXLicense(license, licenses, licenseLocations, checked) - continue - } - - licenses = addNonSPDXLicense(licenses, license, licenseLocations) - } - return licenses -} - -func handleSPDXLicense(license syftPkg.License, licenses []grant.License, licenseLocations []string, checked map[string]bool) []grant.License { - extractedLicenses, err := spdxexp.ExtractLicenses(license.SPDXExpression) - if err != nil { - // log.Errorf("unable to extract licenses from SPDX expression: %s", license.SPDXExpression) - return addNonSPDXLicense(licenses, license, licenseLocations) - } - - // process each extracted license from the SPDX expression - for _, extractedLicense := range extractedLicenses { - extractedLicense = strings.TrimRight(extractedLicense, "+") - // prevent duplicates from being added when using SPDX expressions - // EG: "MIT AND MIT" is valid, but we want to de-duplicate these - if check(checked, extractedLicense) { - continue - } - - // we have what seems to be a valid SPDX license ID, let's try and get more info about it - spdxLicense, err := spdxlicense.GetLicenseByID(extractedLicense) - if err != nil { - log.Errorf("unable to get license by ID: %s; no matching spdx id found", extractedLicense) - // if we can't find a matching SPDX license, just add the license as-is - // TODO: best matching against the spdx list index - addNonSPDXLicense(licenses, license, licenseLocations) - continue - } - - licenses = append(licenses, grant.License{ - SPDXExpression: extractedLicense, - Name: spdxLicense.Name, - Locations: licenseLocations, - Reference: spdxLicense.Reference, - IsDeprecatedLicenseID: spdxLicense.IsDeprecatedLicenseID, - DetailsURL: spdxLicense.DetailsURL, - ReferenceNumber: spdxLicense.ReferenceNumber, - LicenseID: spdxLicense.LicenseID, - SeeAlso: spdxLicense.SeeAlso, - IsOsiApproved: spdxLicense.IsOsiApproved, - }) - } - return licenses -} - -func addNonSPDXLicense(licenses []grant.License, license syftPkg.License, locations []string) []grant.License { - return append(licenses, grant.License{ - Name: license.Value, - Locations: locations, - }) -} - -func check(checked map[string]bool, license string) bool { - if _, ok := checked[license]; !ok { - checked[license] = true - return false - } - return true -} diff --git a/grant/license.go b/grant/license.go index f86cd70..bd857d5 100644 --- a/grant/license.go +++ b/grant/license.go @@ -1,5 +1,15 @@ package grant +import ( + "strings" + + "github.com/github/go-spdx/v2/spdxexp" + + "github.com/anchore/grant/internal/log" + "github.com/anchore/grant/internal/spdxlicense" + syftPkg "github.com/anchore/syft/syft/pkg" +) + type LicenseID string // License is a grant license. Either SPDXExpression or Name will be set. @@ -38,3 +48,87 @@ func (l License) String() string { func (l License) IsSPDX() bool { return l.SPDXExpression != "" } + +// ConvertSyftLicenses converts a syft LicenseSet to a grant License slice +// note: syft licenses can sometimes have complex SPDX expressions. +// Grant licenses break down these expressions into individual licenses. +// Because license expressions could potentially contain multiple licenses +// that are already represented in the syft license set we need to de-duplicate +// syft licenses have a "Value" field which is the name of the license +// given to an invalid SPDX expression; grant licenses store this field as "Name" +func ConvertSyftLicenses(set syftPkg.LicenseSet) (licenses []License) { + licenses = make([]License, 0) + checked := make(map[string]bool) + for _, license := range set.ToSlice() { + locations := license.Locations.ToSlice() + licenseLocations := make([]string, 0) + for _, location := range locations { + licenseLocations = append(licenseLocations, location.RealPath) + } + + if license.SPDXExpression != "" { + licenses = handleSPDXLicense(license, licenses, licenseLocations, checked) + continue + } + + licenses = addNonSPDXLicense(licenses, license, licenseLocations) + } + return licenses +} + +func handleSPDXLicense(license syftPkg.License, licenses []License, licenseLocations []string, checked map[string]bool) []License { + extractedLicenses, err := spdxexp.ExtractLicenses(license.SPDXExpression) + if err != nil { + // log.Errorf("unable to extract licenses from SPDX expression: %s", license.SPDXExpression) + return addNonSPDXLicense(licenses, license, licenseLocations) + } + + // process each extracted license from the SPDX expression + for _, extractedLicense := range extractedLicenses { + extractedLicense = strings.TrimRight(extractedLicense, "+") + // prevent duplicates from being added when using SPDX expressions + // EG: "MIT AND MIT" is valid, but we want to de-duplicate these + if check(checked, extractedLicense) { + continue + } + + // we have what seems to be a valid SPDX license ID, let's try and get more info about it + spdxLicense, err := spdxlicense.GetLicenseByID(extractedLicense) + if err != nil { + log.Errorf("unable to get license by ID: %s; no matching spdx id found", extractedLicense) + // if we can't find a matching SPDX license, just add the license as-is + // TODO: best matching against the spdx list index + addNonSPDXLicense(licenses, license, licenseLocations) + continue + } + + licenses = append(licenses, License{ + SPDXExpression: extractedLicense, + Name: spdxLicense.Name, + Locations: licenseLocations, + Reference: spdxLicense.Reference, + IsDeprecatedLicenseID: spdxLicense.IsDeprecatedLicenseID, + DetailsURL: spdxLicense.DetailsURL, + ReferenceNumber: spdxLicense.ReferenceNumber, + LicenseID: spdxLicense.LicenseID, + SeeAlso: spdxLicense.SeeAlso, + IsOsiApproved: spdxLicense.IsOsiApproved, + }) + } + return licenses +} + +func addNonSPDXLicense(licenses []License, license syftPkg.License, locations []string) []License { + return append(licenses, License{ + Name: license.Value, + Locations: locations, + }) +} + +func check(checked map[string]bool, license string) bool { + if _, ok := checked[license]; !ok { + checked[license] = true + return false + } + return true +} diff --git a/grant/package.go b/grant/package.go index 14db4a1..e2378b5 100644 --- a/grant/package.go +++ b/grant/package.go @@ -1,5 +1,7 @@ package grant +import syftPkg "github.com/anchore/syft/syft/pkg" + // PackageID is a unique identifier for a package that is tracked by grant // It's usually provided by the SBOM; It's calculated if an SBOM is generated type PackageID string @@ -12,3 +14,18 @@ type Package struct { Licenses []License `json:"licenses" yaml:"licenses"` Locations []string `json:"locations" yaml:"locations"` } + +func ConvertSyftPackage(p syftPkg.Package) *Package { + locations := p.Locations.ToSlice() + packageLocations := make([]string, 0) + for _, location := range locations { + packageLocations = append(packageLocations, location.RealPath) + } + + return &Package{ + Name: p.Name, + Version: p.Version, + Licenses: ConvertSyftLicenses(p.Licenses), + Locations: packageLocations, + } +} From 988e972b0a4e6439ebcfec84d672c77ae605b736 Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Wed, 24 Jan 2024 11:37:03 -0500 Subject: [PATCH 3/5] feat: more render for list Signed-off-by: Christopher Phillips --- cmd/grant/cli/internal/list/report.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/cmd/grant/cli/internal/list/report.go b/cmd/grant/cli/internal/list/report.go index 1fe7827..a80d633 100644 --- a/cmd/grant/cli/internal/list/report.go +++ b/cmd/grant/cli/internal/list/report.go @@ -6,6 +6,9 @@ import ( "fmt" "time" + "github.com/gookit/color" + "github.com/jedib0t/go-pretty/v6/list" + "github.com/anchore/grant/cmd/grant/cli/internal" "github.com/anchore/grant/event" "github.com/anchore/grant/grant" @@ -103,11 +106,23 @@ func (r *Report) renderJSON() error { } func (r *Report) renderList() error { - _ = Response{ - ReportID: r.ReportID, - Timestamp: r.Timestamp, - Inputs: make([]string, 0), - Results: make([]Result, 0), + var uiLists []list.Writer + for _, c := range r.Cases { + r.Monitor.Increment() + r.Monitor.AtomicStage.Set(c.UserInput) + resultList := list.NewWriter() + uiLists = append(uiLists, resultList) + resultList.AppendItem(color.Primary.Sprintf("%s", c.UserInput)) + licenses := c.GetLicenses() + resultList.Indent() + for _, l := range licenses { + resultList.AppendItem(fmt.Sprintf("%s", l.Name)) + } + resultList.UnIndent() + } + + for _, l := range uiLists { + bus.Report(l.Render()) } return nil } From fa076a4184818a44baf0b6d660166c8c8352719d Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Wed, 24 Jan 2024 13:05:32 -0500 Subject: [PATCH 4/5] chore: update linter simplifications Signed-off-by: Christopher Phillips --- cmd/grant/cli/internal/check/report.go | 37 -------------------------- cmd/grant/cli/internal/list/report.go | 2 +- grant/case.go | 9 ++----- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 604c91e..7daece7 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -186,43 +186,6 @@ func (r *Report) renderCheckTree() error { return nil } -func (r *Report) renderList() error { - var uiLists []list.Writer - for _, res := range r.Results { - r.Monitor.Increment() - r.Monitor.AtomicStage.Set(res.Case.UserInput) - resulList := newList() - uiLists = append(uiLists, resulList) - resulList.AppendItem(color.Primary.Sprintf("%s", res.Case.UserInput)) - for _, license := range res.Evaluations.GetLicenses() { - resulList.Indent() - resulList.AppendItem(color.Light.Sprintf("%s", license)) - resulList.UnIndent() - if r.Config.Options.ShowPackages { - packages := res.Evaluations.Packages(license) - resulList.Indent() - resulList.Indent() - for _, pkg := range packages { - resulList.AppendItem(color.Secondary.Sprintf("%s", pkg)) - } - resulList.UnIndent() - resulList.UnIndent() - } - } - if r.Config.Options.ShowPackages { - renderOrphanPackages(resulList, res, true) - } - } - r.Monitor.AtomicStage.Set(strings.Join(r.Results.UserInputs(), ", ")) - - // segment the results into lists by user input - // lists can optionally show the packages that were evaluated - for _, l := range uiLists { - bus.Report(l.Render()) - } - return nil -} - func renderOrphanPackages(l list.Writer, res evalutation.Result, invert bool) { title := color.Secondary newItem := color.Light diff --git a/cmd/grant/cli/internal/list/report.go b/cmd/grant/cli/internal/list/report.go index a80d633..a93dd08 100644 --- a/cmd/grant/cli/internal/list/report.go +++ b/cmd/grant/cli/internal/list/report.go @@ -116,7 +116,7 @@ func (r *Report) renderList() error { licenses := c.GetLicenses() resultList.Indent() for _, l := range licenses { - resultList.AppendItem(fmt.Sprintf("%s", l.Name)) + resultList.AppendItem(l.Name) } resultList.UnIndent() } diff --git a/grant/case.go b/grant/case.go index 544504c..6f51b2b 100644 --- a/grant/case.go +++ b/grant/case.go @@ -65,16 +65,11 @@ func (c Case) GetLicenses() []License { continue } - for _, l := range grantPkg.Licenses { - licenses = append(licenses, l) - } + licenses = append(licenses, grantPkg.Licenses...) } } - for _, l := range c.Licenses { - licenses = append(licenses, l) - } - + licenses = append(licenses, c.Licenses...) return licenses } From ee8b539ba93b149368f6bdb52354696b21b8b9b8 Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Wed, 24 Jan 2024 13:11:38 -0500 Subject: [PATCH 5/5] test: refactor policy out of case test Signed-off-by: Christopher Phillips --- grant/evalutation/result_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grant/evalutation/result_test.go b/grant/evalutation/result_test.go index 327db4b..50c48ea 100644 --- a/grant/evalutation/result_test.go +++ b/grant/evalutation/result_test.go @@ -25,7 +25,7 @@ func Test_NewResults(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - cases := grant.NewCases(tc.ec.Policy, tc.fixtures...) + cases := grant.NewCases(tc.fixtures...) results := NewResults(tc.ec, cases...) if tc.isFailed != results.IsFailed() { t.Errorf("results.IsFailed() = %v, want %v", results.IsFailed(), tc.isFailed)