From 6f4ed8233f5f9b06299ab192c7038f86cef79bd9 Mon Sep 17 00:00:00 2001 From: Christopher Phillips Date: Tue, 23 Jan 2024 11:55:27 -0500 Subject: [PATCH] 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 }