diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index 8c1a66e..1c891c1 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -42,8 +42,6 @@ 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, userInput []string) (errs error) { // check if user provided source by stdin // note: cat sbom.json | grant check spdx.json - is supported diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 2d0ad09..32440d2 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -69,7 +69,7 @@ func (r *Report) renderJSON(out io.Writer) error { } func (r *Report) renderTable(out io.Writer) error { - return errors.New("not implemented") + } // l := list.NewWriter() // TODO: style me diff --git a/grant/case.go b/grant/case.go index d5eaa9a..8c7ad5b 100644 --- a/grant/case.go +++ b/grant/case.go @@ -25,8 +25,6 @@ import ( // Case is a collection of SBOMs and Licenses that are evaluated against a policy type Case struct { - // CaseID is the unique identifier for the case - CaseID string // SBOMS is a list of SBOMs that have licenses checked against the policy SBOMS []sbom.SBOM diff --git a/grant/evalutation/license_evaluation_test.go b/grant/evalutation/license_evaluation_test.go new file mode 100644 index 0000000..9b9205f --- /dev/null +++ b/grant/evalutation/license_evaluation_test.go @@ -0,0 +1,45 @@ +package evalutation + +import ( + "testing" + + "github.com/anchore/grant/grant" +) + +func Test_NewLicenseEvaluations(t *testing.T) { + tests := []struct { + name string + config EvaluationConfig + caseFixture string + wantFailed bool + }{ + { + name: "NewLicenseEvaluations returns a slice of LicenseEvaluation that fail for the DefaultPolicy", + config: DefaultEvaluationConfig(), + caseFixture: "../../fixtures/multiple", + wantFailed: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + grantCases := fixtureCase(tc.config, tc.caseFixture) + for _, c := range grantCases { + caseEvaluations := NewLicenseEvaluations(tc.config, c) + if len(caseEvaluations) == 0 { + t.Fatal("could not build license evaluations") + } + if len(caseEvaluations.Licenses()) == 0 { + t.Fatal("could not build list of licenses from evaluations") + } + if tc.wantFailed && !caseEvaluations.IsFailed() { + t.Fatal("expected license evaluations to fail for default config") + } + } + }) + } +} + +func fixtureCase(ec EvaluationConfig, fixturePath string) []grant.Case { + return grant.NewCases(&ec.Policy, fixturePath) +} diff --git a/grant/evalutation/license_evalutation.go b/grant/evalutation/license_evalutation.go index d1fde7a..ae3f620 100644 --- a/grant/evalutation/license_evalutation.go +++ b/grant/evalutation/license_evalutation.go @@ -2,84 +2,94 @@ package evalutation import ( "github.com/anchore/grant/grant" + "github.com/anchore/syft/syft/sbom" ) func NewLicenseEvaluations(ec EvaluationConfig, c grant.Case) LicenseEvaluations { evaluations := make([]LicenseEvaluation, 0) - // TODO: probably want to use some concurrency here for _, sb := range c.SBOMS { - for pkg := range sb.Artifacts.Packages.Enumerate() { - grantPkg := convertSyftPackage(pkg) - // since we use syft as a library to generate the sbom we need to convert its packages/licenses to grant types - if len(grantPkg.Licenses) == 0 { - evaluations = append(evaluations, LicenseEvaluation{ - License: grant.License{}, - Package: grantPkg, - Policy: ec.Policy, - Reason: []Reason{ReasonNoLicenseFound}, - Pass: true, - }) - continue - } - - for _, l := range grantPkg.Licenses { - if !l.IsSPDX() { - // TODO: check if the config wants us to check for non-SPDX licenses - } - if ec.Policy.IsDenied(l) { - evaluations = append(evaluations, LicenseEvaluation{ - License: l, - Package: grantPkg, - Policy: ec.Policy, - Reason: []Reason{ReasonLicenseDenied}, - Pass: false, - }) - continue - } - // otherwise, the license is allowed - evaluations = append(evaluations, LicenseEvaluation{ - License: l, - Package: grantPkg, - Policy: ec.Policy, - Reason: []Reason{ReasonLicenseAllowed}, - Pass: true, - }) - } - } + evaluations = checkSBOM(ec, c, sb, evaluations) } for _, l := range c.Licenses { - if !l.IsSPDX() { - // TODO: check if the config wants us to check for non-SPDX licenses - } - if ec.Policy.IsDenied(l) { - evaluations = append(evaluations, LicenseEvaluation{ - License: l, - Package: nil, - Policy: ec.Policy, - Reason: []Reason{ReasonLicenseDenied}, - Pass: false, - }) + evaluations = checkLicense(ec, nil, l, evaluations) + } + + return evaluations +} + +func checkSBOM(ec EvaluationConfig, c grant.Case, sb sbom.SBOM, evaluations []LicenseEvaluation) []LicenseEvaluation { + 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) + if len(grantPkg.Licenses) == 0 { + le := NewLicenseEvaluation(grant.License{}, grantPkg, ec.Policy, []Reason{ReasonNoLicenseFound}, true) + evaluations = append(evaluations, le) continue } - // otherwise, the license is allowed - evaluations = append(evaluations, LicenseEvaluation{ - License: l, - Package: nil, - Policy: ec.Policy, - Reason: []Reason{ReasonLicenseAllowed}, - Pass: true, - }) - } + for _, l := range grantPkg.Licenses { + evaluations = checkLicense(ec, grantPkg, l, evaluations) + } + } return evaluations } +func checkLicense(ec EvaluationConfig, pkg *grant.Package, l grant.License, evaluations []LicenseEvaluation) []LicenseEvaluation { + if !l.IsSPDX() { + // TODO: check if the config wants us to check for non-SPDX licenses + } + if ec.Policy.IsDenied(l) { + le := NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{ReasonLicenseDenied}, false) + return append(evaluations, le) + } + le := NewLicenseEvaluation(l, pkg, ec.Policy, []Reason{ReasonLicenseAllowed}, true) + return append(evaluations, le) +} + type LicenseEvaluations []LicenseEvaluation -type LicenseEvaluation struct { - RequestID string +func (le LicenseEvaluations) Packages() []grant.Package { + packages := make([]grant.Package, 0) + // get the set of unique packages from the list... + for _, e := range le { + if e.Package != nil { + packages = append(packages, *e.Package) + } + } + return packages +} +func (le LicenseEvaluations) Licenses() []grant.License { + licenses := make([]grant.License, 0) + // get the set of unique licenses from the list... + for _, e := range le { + licenses = append(licenses, e.License) + } + return licenses +} + +func (le LicenseEvaluations) FailedLicenses() []grant.License { + licenses := make([]grant.License, 0) + // get the set of unique licenses from the list... + for _, e := range le { + if !e.Pass { + licenses = append(licenses, e.License) + } + } + return licenses +} + +func (le LicenseEvaluations) IsFailed() bool { + for _, e := range le { + if !e.Pass { + return true + } + } + return false +} + +type LicenseEvaluation struct { // inputs into evaluation... License grant.License // the license that we evaluated Package *grant.Package // any artifact license is evaluated with @@ -92,23 +102,12 @@ type LicenseEvaluation struct { Pass bool // The final evaluation } -func (ds LicenseEvaluations) Packages() []grant.Package { - // get the set of unique packages from the list... - panic("not implemented") - -} - -func (ds LicenseEvaluations) Licenses() []grant.License { - // get the set of unique license from the list... - panic("not implemented") - -} - -func (ds LicenseEvaluations) Policies() []grant.Policy { - // get the set of unique policies from the list... - panic("not implemented") -} - -func (ds LicenseEvaluations) IsFailed() bool { - panic("not implemented") +func NewLicenseEvaluation(license grant.License, pkg *grant.Package, policy grant.Policy, reasons []Reason, pass bool) LicenseEvaluation { + return LicenseEvaluation{ + License: license, + Package: pkg, + Policy: policy, + Reason: reasons, + Pass: pass, + } } diff --git a/grant/evalutation/license_evalutation_config.go b/grant/evalutation/license_evalutation_config.go index c1e4585..a8c2fe9 100644 --- a/grant/evalutation/license_evalutation_config.go +++ b/grant/evalutation/license_evalutation_config.go @@ -9,3 +9,10 @@ type EvaluationConfig struct { // CheckNonSPDX is true if non-SPDX licenses should be checked CheckNonSPDX bool } + +func DefaultEvaluationConfig() EvaluationConfig { + return EvaluationConfig{ + Policy: grant.DefaultPolicy(), + CheckNonSPDX: false, + } +} diff --git a/grant/evalutation/result.go b/grant/evalutation/result.go index c346163..4b9dbe4 100644 --- a/grant/evalutation/result.go +++ b/grant/evalutation/result.go @@ -31,5 +31,10 @@ func NewResults(ec EvaluationConfig, cases ...grant.Case) (r Results) { // Pass T/F + reasons for failure // Validate() error ([]string reasons) func (rs Results) Pass() bool { - panic("not implemented") + for _, r := range rs { + if r.Evaluations.IsFailed() { + return false + } + } + return true } diff --git a/grant/evalutation/result_test.go b/grant/evalutation/result_test.go new file mode 100644 index 0000000..0ef8d15 --- /dev/null +++ b/grant/evalutation/result_test.go @@ -0,0 +1,35 @@ +package evalutation + +import ( + "testing" + + "github.com/anchore/grant/grant" +) + +func Test_NewResults(t *testing.T) { + tests := []struct { + name string + ec EvaluationConfig + fixtures []string + wantPass bool + }{ + { + name: "NewResults returns results from a group of cases that cannot pass the default config", + ec: DefaultEvaluationConfig(), + fixtures: []string{ + "../../fixtures/multiple", + "../../fixtures/licenses/MIT", + }, + wantPass: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cases := grant.NewCases(&tc.ec.Policy, tc.fixtures...) + results := NewResults(tc.ec, cases...) + if tc.wantPass != results.Pass() { + t.Errorf("NewResults() = %v, want %v", results.Pass(), tc.wantPass) + } + }) + } +} diff --git a/grant/policy.go b/grant/policy.go index d129354..270b341 100644 --- a/grant/policy.go +++ b/grant/policy.go @@ -16,8 +16,8 @@ type Policy struct { } // DefaultPolicy returns a policy that denies all licenses -func DefaultPolicy() *Policy { - return &Policy{ +func DefaultPolicy() Policy { + return Policy{ AllowLicenses: make([]glob.Glob, 0), DenyLicenses: []glob.Glob{ glob.MustCompile("*"), @@ -31,7 +31,8 @@ func DefaultPolicy() *Policy { // It lower cases all patterns to make matching against the spdx license set case-insensitive func NewPolicy(allowLicenses, denyLicenses, ignoreLicenses []string) (p *Policy, err error) { if len(allowLicenses) == 0 && len(denyLicenses) == 0 { - return DefaultPolicy(), nil + defaultPolicy := DefaultPolicy() + return &defaultPolicy, nil } var (