Raw results for license (#1790)

* Raw results for license

* tests

* tests

* e2e fix

* comment

* fix

* linter
This commit is contained in:
laurentsimon 2022-04-13 18:20:05 -07:00 committed by GitHub
parent c0e41f3a54
commit 4d1c531690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 348 additions and 209 deletions

View File

@ -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 {

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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()

149
checks/raw/license.go Normal file
View File

@ -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
}

View File

@ -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()
}
})
}
}

View File

@ -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())

View File

@ -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())
}