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..7c97271 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" @@ -114,12 +115,14 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { } reportConfig := check.ReportConfig{ - Policy: policy, - Format: check.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 bd4fc5c..dcc1858 100644 --- a/cmd/grant/cli/command/list.go +++ b/cmd/grant/cli/command/list.go @@ -7,10 +7,10 @@ import ( "github.com/spf13/cobra" "github.com/anchore/clio" - "github.com/anchore/grant/cmd/grant/cli/internal/check" + "github.com/anchore/grant/cmd/grant/cli/internal" + "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" ) @@ -69,16 +69,17 @@ func runList(cfg *ListConfig, userInput []string) (errs error) { } }() - reportConfig := check.ReportConfig{ - Format: check.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/format.go b/cmd/grant/cli/internal/check/format.go deleted file mode 100644 index 6fea576..0000000 --- a/cmd/grant/cli/internal/check/format.go +++ /dev/null @@ -1,20 +0,0 @@ -package check - -type Format string - -const ( - JSON Format = "json" - Table Format = "table" -) - -// validFormat returns a valid format or the default format if the given format is invalid -func validateFormat(f Format) Format { - switch f { - case "json": - return JSON - case "table": - return Table - default: - return Table - } -} diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 4819de5..7daece7 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -3,12 +3,14 @@ package check import ( "encoding/json" "errors" + "fmt" "strings" "time" "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,15 +33,12 @@ 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. +// 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 @@ -50,12 +49,12 @@ func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { rc.Policy = grant.DefaultPolicy() } - rc.Format = validateFormat(rc.Format) - cases := grant.NewCases(rc.Policy, userRequests...) + rc.Options.Format = internal.ValidateFormat(rc.Options.Format) + cases := grant.NewCases(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 +69,25 @@ 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() + 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.Format { - case Table: - return r.renderList() - case 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 +95,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 +106,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 +117,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(), @@ -157,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() @@ -165,9 +156,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 +169,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 } } @@ -195,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.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.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/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/format.go b/cmd/grant/cli/internal/format.go new file mode 100644 index 0000000..96b3b3d --- /dev/null +++ b/cmd/grant/cli/internal/format.go @@ -0,0 +1,28 @@ +package internal + +import "github.com/google/uuid" + +type Format string + +const ( + JSON Format = "json" + Table Format = "table" +) + +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": + return JSON + case "table": + return Table + default: + 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 new file mode 100644 index 0000000..a93dd08 --- /dev/null +++ b/cmd/grant/cli/internal/list/report.go @@ -0,0 +1,128 @@ +package list + +import ( + "encoding/json" + "errors" + "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" + "github.com/anchore/grant/internal/bus" +) + +type Report struct { + ReportID string + Cases []grant.Case + 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 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 +// 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) + cases := grant.NewCases(userRequests...) + + return &Report{ + 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 { + 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(l.Name) + } + resultList.UnIndent() + } + + for _, l := range uiLists { + bus.Report(l.Render()) + } + return 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/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 1c29340..6f51b2b 100644 --- a/grant/case.go +++ b/grant/case.go @@ -24,23 +24,19 @@ 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 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 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,13 +50,29 @@ 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) } 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 + } + + licenses = append(licenses, grantPkg.Licenses...) + } + } + + licenses = append(licenses, c.Licenses...) + return licenses +} + type CaseHandler struct { Backend *backend.ClassifierBackend } @@ -153,13 +165,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 @@ -184,7 +196,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) @@ -203,12 +214,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 } 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/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) 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, + } +}