From 4d1c5316907e496cb98bcb397c17b994f5c4f296 Mon Sep 17 00:00:00 2001 From: laurentsimon <64505099+laurentsimon@users.noreply.github.com> Date: Wed, 13 Apr 2022 18:20:05 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Raw=20results=20for=20license=20(#1?= =?UTF-8?q?790)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Raw results for license * tests * tests * e2e fix * comment * fix * linter --- checker/raw_result.go | 7 ++ checks/evaluation/license.go | 45 +++++++++++ checks/license.go | 139 ++++---------------------------- checks/license_test.go | 82 +------------------ checks/raw/license.go | 149 +++++++++++++++++++++++++++++++++++ checks/raw/license_test.go | 99 +++++++++++++++++++++++ e2e/license_test.go | 6 +- pkg/json_raw_results.go | 30 ++++++- 8 files changed, 348 insertions(+), 209 deletions(-) create mode 100644 checks/evaluation/license.go create mode 100644 checks/raw/license.go create mode 100644 checks/raw/license_test.go diff --git a/checker/raw_result.go b/checker/raw_result.go index c27b6a18..3e7cd484 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -29,6 +29,7 @@ type RawResults struct { WebhookResults WebhooksData MaintainedResults MaintainedData SignedReleasesResults SignedReleasesData + LicenseResults LicenseData } // MaintainedData contains the raw results @@ -39,6 +40,12 @@ type MaintainedData struct { ArchivedStatus ArchivedStatus } +// LicenseData contains the raw results +// for the License check. +type LicenseData struct { + Files []File +} + // CodeReviewData contains the raw results // for the Code-Review check. type CodeReviewData struct { diff --git a/checks/evaluation/license.go b/checks/evaluation/license.go new file mode 100644 index 00000000..d5ad18ee --- /dev/null +++ b/checks/evaluation/license.go @@ -0,0 +1,45 @@ +// Copyright 2021 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package evaluation + +import ( + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" +) + +// License applies the score policy for the License check. +func License(name string, dl checker.DetailLogger, + r *checker.LicenseData, +) checker.CheckResult { + if r == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") + return checker.CreateRuntimeErrorResult(name, e) + } + + // Apply the policy evaluation. + if r.Files == nil || len(r.Files) == 0 { + return checker.CreateMinScoreResult(name, "license file not detected") + } + + for _, f := range r.Files { + dl.Info(&checker.LogMessage{ + Path: f.Path, + Type: checker.FileTypeSource, + Offset: 1, + }) + } + + return checker.CreateMaxScoreResult(name, "license file detected") +} diff --git a/checks/license.go b/checks/license.go index a3b242f2..6778b92f 100644 --- a/checks/license.go +++ b/checks/license.go @@ -15,22 +15,12 @@ package checks import ( - "fmt" - "regexp" - "strings" - "github.com/ossf/scorecard/v4/checker" - "github.com/ossf/scorecard/v4/checks/fileparser" + "github.com/ossf/scorecard/v4/checks/evaluation" + "github.com/ossf/scorecard/v4/checks/raw" + sce "github.com/ossf/scorecard/v4/errors" ) -type check func(str string, extCheck []string) bool - -type checks struct { - rstr string // regex string - f check - p []string -} - // CheckLicense is the registered name for License. const CheckLicense = "License" @@ -40,123 +30,24 @@ func init() { checker.FileBased, checker.CommitBased, } - if err := registerCheck(CheckLicense, LicenseCheck, supportedRequestTypes); err != nil { + if err := registerCheck(CheckLicense, License, supportedRequestTypes); err != nil { // this should never happen panic(err) } } -const ( - copying = "copy(ing|right)" - license = "(un)?licen[sc]e" - preferredExt = "*\\.(md|markdown|html)$" - anyExt = ".[^./]" - ofl = "ofl" - patents = "patents" -) - -// Regex converted from -// https://github.com/licensee/licensee/blob/master/lib/licensee/project_files/license_file.rb -var ( - extensions = []string{"xml", "go", "gemspec"} - regexChecks = []checks{ - {rstr: copying, f: nil}, - {rstr: license, f: nil}, - {rstr: license + preferredExt, f: nil}, - {rstr: copying + preferredExt, f: nil}, - {rstr: copying + anyExt, f: nil}, - {rstr: ofl, f: nil}, - {rstr: ofl + preferredExt, f: nil}, - {rstr: patents, f: nil}, - {rstr: license, f: extensionMatch, p: []string{"spdx", "header"}}, - {rstr: license + "[-_][^.]*", f: extensionMatch, p: extensions}, - {rstr: copying + "[-_][^.]*", f: extensionMatch, p: extensions}, - {rstr: "\\w+[-_]" + license + "[^.]*", f: extensionMatch, p: extensions}, - {rstr: "\\w+[-_]" + copying + "[^.]*", f: extensionMatch, p: extensions}, - {rstr: ofl, f: extensionMatch, p: extensions}, - } -) - -// ExtensionMatch to check for matching extension. -func extensionMatch(f string, exts []string) bool { - s := strings.Split(f, ".") - - if len(s) <= 1 { - return false - } - - fext := s[len(s)-1] - - found := false - for _, ext := range exts { - if ext == fext { - found = true - break - } - } - - return found -} - -// TestLicenseCheck used for testing purposes. -func testLicenseCheck(name string) bool { - return checkLicense(name) -} - -// LicenseCheck runs LicenseCheck check. -func LicenseCheck(c *checker.CheckRequest) checker.CheckResult { - var s string - - err := fileparser.OnAllFilesDo(c.RepoClient, isLicenseFile, &s) +// License runs License check. +func License(c *checker.CheckRequest) checker.CheckResult { + rawData, err := raw.License(c) if err != nil { - return checker.CreateRuntimeErrorResult(CheckLicense, err) + e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + return checker.CreateRuntimeErrorResult(CheckLicense, e) } - if s != "" { - c.Dlogger.Info(&checker.LogMessage{ - Path: s, - Type: checker.FileTypeSource, - Offset: 1, - }) - return checker.CreateMaxScoreResult(CheckLicense, "license file detected") + + // Set the raw results. + if c.RawResults != nil { + c.RawResults.LicenseResults = rawData } - return checker.CreateMinScoreResult(CheckLicense, "license file not detected") -} - -var isLicenseFile fileparser.DoWhileTrueOnFilename = func(name string, args ...interface{}) (bool, error) { - if len(args) != 1 { - return false, fmt.Errorf("isLicenseFile requires exactly one argument: %w", errInvalidArgLength) - } - s, ok := args[0].(*string) - if !ok { - return false, fmt.Errorf("isLicenseFile requires argument of type: *string: %w", errInvalidArgType) - } - if checkLicense(name) { - if s != nil { - *s = name - } - return false, nil - } - return true, nil -} - -// CheckLicense to check whether the name parameter fulfill license file criteria. -func checkLicense(name string) bool { - for _, check := range regexChecks { - rg := regexp.MustCompile(check.rstr) - - nameLower := strings.ToLower(name) - t := rg.MatchString(nameLower) - if t { - extFound := true - - // check extension calling f function. - // f function will always be func extensionMatch(..) - if check.f != nil { - extFound = check.f(nameLower, check.p) - } - - return extFound - } - } - return false + + return evaluation.License(CheckLicense, c.Dlogger, &rawData) } diff --git a/checks/license_test.go b/checks/license_test.go index d778bf15..450b8626 100644 --- a/checks/license_test.go +++ b/checks/license_test.go @@ -28,86 +28,6 @@ import ( scut "github.com/ossf/scorecard/v4/utests" ) -func TestLicenseFileCheck(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - }{ - { - name: "LICENSE.md", - filename: "LICENSE.md", - }, - { - name: "LICENSE", - filename: "LICENSE", - }, - { - name: "COPYING", - filename: "COPYING", - }, - { - name: "COPYING.md", - filename: "COPYING.md", - }, - { - name: "LICENSE.textile", - filename: "LICENSE.textile", - }, - { - name: "COPYING.textile", - filename: "COPYING.textile", - }, - { - name: "LICENSE-MIT", - filename: "LICENSE-MIT", - }, - { - name: "COPYING-MIT", - filename: "COPYING-MIT", - }, - { - name: "MIT-LICENSE-MIT", - filename: "MIT-LICENSE-MIT", - }, - { - name: "MIT-COPYING", - filename: "MIT-COPYING", - }, - { - name: "OFL.md", - filename: "OFL.md", - }, - { - name: "OFL.textile", - filename: "OFL.textile", - }, - { - name: "OFL", - filename: "OFL", - }, - { - name: "PATENTS", - filename: "PATENTS", - }, - { - name: "PATENTS.txt", - filename: "PATENTS.txt", - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - s := testLicenseCheck(tt.filename) - if !s { - t.Fail() - } - }) - } -} - func TestLicenseFileSubdirectory(t *testing.T) { t.Parallel() @@ -167,7 +87,7 @@ func TestLicenseFileSubdirectory(t *testing.T) { Dlogger: &dl, } - res := LicenseCheck(&req) + res := License(&req) if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &res, &dl) { t.Fail() diff --git a/checks/raw/license.go b/checks/raw/license.go new file mode 100644 index 00000000..61f66ee7 --- /dev/null +++ b/checks/raw/license.go @@ -0,0 +1,149 @@ +// Copyright 2021 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package raw + +import ( + "fmt" + "regexp" + "strings" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/checks/fileparser" +) + +type check func(str string, extCheck []string) bool + +type checks struct { + rstr string // regex string + f check + p []string +} + +const ( + copying = "copy(ing|right)" + license = "(un)?licen[sc]e" + preferredExt = "*\\.(md|markdown|html)$" + anyExt = ".[^./]" + ofl = "ofl" + patents = "patents" +) + +// Regex converted from +// https://github.com/licensee/licensee/blob/master/lib/licensee/project_files/license_file.rb +var ( + extensions = []string{"xml", "go", "gemspec"} + regexChecks = []checks{ + {rstr: copying, f: nil}, + {rstr: license, f: nil}, + {rstr: license + preferredExt, f: nil}, + {rstr: copying + preferredExt, f: nil}, + {rstr: copying + anyExt, f: nil}, + {rstr: ofl, f: nil}, + {rstr: ofl + preferredExt, f: nil}, + {rstr: patents, f: nil}, + {rstr: license, f: extensionMatch, p: []string{"spdx", "header"}}, + {rstr: license + "[-_][^.]*", f: extensionMatch, p: extensions}, + {rstr: copying + "[-_][^.]*", f: extensionMatch, p: extensions}, + {rstr: "\\w+[-_]" + license + "[^.]*", f: extensionMatch, p: extensions}, + {rstr: "\\w+[-_]" + copying + "[^.]*", f: extensionMatch, p: extensions}, + {rstr: ofl, f: extensionMatch, p: extensions}, + } +) + +// License retrieves the raw data for the License check. +func License(c *checker.CheckRequest) (checker.LicenseData, error) { + var results checker.LicenseData + var path string + + err := fileparser.OnAllFilesDo(c.RepoClient, isLicenseFile, &path) + if err != nil { + return results, fmt.Errorf("fileparser.OnAllFilesDo: %w", err) + } + + if path != "" { + results.Files = append(results.Files, + checker.File{ + Path: path, + Type: checker.FileTypeSource, + }) + } + + return results, nil +} + +// ExtensionMatch to check for matching extension. +func extensionMatch(f string, exts []string) bool { + s := strings.Split(f, ".") + + if len(s) <= 1 { + return false + } + + fext := s[len(s)-1] + + found := false + for _, ext := range exts { + if ext == fext { + found = true + break + } + } + + return found +} + +// TestLicense used for testing purposes. +func TestLicense(name string) bool { + return checkLicense(name) +} + +var isLicenseFile fileparser.DoWhileTrueOnFilename = func(name string, args ...interface{}) (bool, error) { + if len(args) != 1 { + return false, fmt.Errorf("isLicenseFile requires exactly one argument: %w", errInvalidArgLength) + } + s, ok := args[0].(*string) + if !ok { + return false, fmt.Errorf("isLicenseFile requires argument of type: *string: %w", errInvalidArgType) + } + if checkLicense(name) { + if s != nil { + *s = name + } + return false, nil + } + return true, nil +} + +// CheckLicense to check whether the name parameter fulfill license file criteria. +func checkLicense(name string) bool { + for _, check := range regexChecks { + rg := regexp.MustCompile(check.rstr) + + nameLower := strings.ToLower(name) + t := rg.MatchString(nameLower) + if t { + extFound := true + + // check extension calling f function. + // f function will always be func extensionMatch(..) + if check.f != nil { + extFound = check.f(nameLower, check.p) + } + + return extFound + } + } + return false +} diff --git a/checks/raw/license_test.go b/checks/raw/license_test.go new file mode 100644 index 00000000..1f0d5920 --- /dev/null +++ b/checks/raw/license_test.go @@ -0,0 +1,99 @@ +// Copyright 2020 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package raw + +import ( + "testing" +) + +func TestLicenseFileCheck(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + filename string + }{ + { + name: "LICENSE.md", + filename: "LICENSE.md", + }, + { + name: "LICENSE", + filename: "LICENSE", + }, + { + name: "COPYING", + filename: "COPYING", + }, + { + name: "COPYING.md", + filename: "COPYING.md", + }, + { + name: "LICENSE.textile", + filename: "LICENSE.textile", + }, + { + name: "COPYING.textile", + filename: "COPYING.textile", + }, + { + name: "LICENSE-MIT", + filename: "LICENSE-MIT", + }, + { + name: "COPYING-MIT", + filename: "COPYING-MIT", + }, + { + name: "MIT-LICENSE-MIT", + filename: "MIT-LICENSE-MIT", + }, + { + name: "MIT-COPYING", + filename: "MIT-COPYING", + }, + { + name: "OFL.md", + filename: "OFL.md", + }, + { + name: "OFL.textile", + filename: "OFL.textile", + }, + { + name: "OFL", + filename: "OFL", + }, + { + name: "PATENTS", + filename: "PATENTS", + }, + { + name: "PATENTS.txt", + filename: "PATENTS.txt", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s := TestLicense(tt.filename) + if !s { + t.Fail() + } + }) + } +} diff --git a/e2e/license_test.go b/e2e/license_test.go index fdcd45c9..8efa1f53 100644 --- a/e2e/license_test.go +++ b/e2e/license_test.go @@ -52,7 +52,7 @@ var _ = Describe("E2E TEST:"+checks.CheckLicense, func() { NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.LicenseCheck(&req) + result := checks.License(&req) Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) @@ -80,7 +80,7 @@ var _ = Describe("E2E TEST:"+checks.CheckLicense, func() { NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.LicenseCheck(&req) + result := checks.License(&req) Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) @@ -120,7 +120,7 @@ var _ = Describe("E2E TEST:"+checks.CheckLicense, func() { NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.LicenseCheck(&req) + result := checks.License(&req) Expect(result.Error).Should(BeNil()) Expect(result.Pass).Should(BeTrue()) diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index c75eeab2..3f150ee4 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -131,8 +131,16 @@ type jsonReleaseAsset struct { URL string `json:"url"` } +//nolint +type jsonLicense struct { + File jsonFile `json:"file"` + // TODO: add fields, like type of license, etc. +} + //nolint type jsonRawResults struct { + // License. + Licenses []jsonLicense `json:"licenses"` // List of recent issues. RecentIssues []jsonIssue `json:"issues"` // List of vulnerabilities. @@ -279,6 +287,21 @@ func (r *jsonScorecardRawResult) addCodeReviewRawResults(cr *checker.CodeReviewD return r.setDefaultCommitData(cr.DefaultBranchCommits) } +//nolint:unparam +func (r *jsonScorecardRawResult) addLicenseRawResults(ld *checker.LicenseData) error { + r.Results.Licenses = []jsonLicense{} + for _, file := range ld.Files { + r.Results.Licenses = append(r.Results.Licenses, + jsonLicense{ + File: jsonFile{ + Path: file.Path, + }, + }, + ) + } + return nil +} + //nolint:unparam func (r *jsonScorecardRawResult) addVulnerbilitiesRawResults(vd *checker.VulnerabilitiesData) error { r.Results.DatabaseVulnerabilities = []jsonDatabaseVulnerability{} @@ -364,7 +387,12 @@ func (r *jsonScorecardRawResult) addBranchProtectionRawResults(bp *checker.Branc } func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) error { - // Vulnerabiliries. + // Licenses. + if err := r.addLicenseRawResults(&raw.LicenseResults); err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + } + + // Vulnerabilities. if err := r.addVulnerbilitiesRawResults(&raw.VulnerabilitiesResults); err != nil { return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) }