Add new checking for license file availability (#1178)

* Add checking logic inside license_check.go
    * Add test case license_check_test.go
    * Add check information inside checks.yaml
This commit is contained in:
Nanik 2021-12-04 04:28:27 +11:00 committed by GitHub
parent 8cb4804c28
commit 45b5a35020
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 427 additions and 0 deletions

147
checks/license_check.go Normal file
View File

@ -0,0 +1,147 @@
// 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 checks
import (
"regexp"
"strings"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks/fileparser"
)
type check func(str string, extCheck []string) bool
type checks struct {
rstr string // regex string
f check
p []string
}
// LicenseCheckPolicy is the registered name for LicenseCheck.
const LicenseCheckPolicy = "LicenseCheck"
//nolint:gochecknoinits
func init() {
registerCheck(LicenseCheckPolicy, LicenseCheck)
}
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 r bool
onFile := func(name string, dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
pdata := fileparser.FileGetCbDataAsBoolPointer(data)
if checkLicense(name) {
c.Dlogger.Info3(&checker.LogMessage{
Path: name,
Type: checker.FileTypeSource,
Offset: 1,
})
*pdata = true
return false, nil
}
return true, nil
}
err := fileparser.CheckIfFileExists(LicenseCheckPolicy, c, onFile, &r)
if err != nil {
return checker.CreateRuntimeErrorResult(LicenseCheckPolicy, err)
}
if r {
return checker.CreateMaxScoreResult(LicenseCheckPolicy, "license file detected")
}
return checker.CreateMinScoreResult(LicenseCheckPolicy, "license file not detected")
}
// 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,184 @@
// 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 checks
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"go.uber.org/zap/zapcore"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/clients/githubrepo"
"github.com/ossf/scorecard/v3/clients/localdir"
scut "github.com/ossf/scorecard/v3/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()
tests := []struct {
name string
inputFolder string
err error
expected scut.TestReturn
}{
{
name: "With LICENSE",
inputFolder: "file://licensedir/withlicense",
expected: scut.TestReturn{
Error: nil,
Score: checker.MaxResultScore,
NumberOfInfo: 1,
},
err: nil,
},
{
name: "Without LICENSE",
inputFolder: "file://licensedir/withoutlicense",
expected: scut.TestReturn{
Error: nil,
Score: checker.MinResultScore,
},
err: nil,
},
}
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()
logger, err := githubrepo.NewLogger(zapcore.DebugLevel)
if err != nil {
t.Errorf("githubrepo.NewLogger: %v", err)
}
// nolint
defer logger.Sync()
ctrl := gomock.NewController(t)
repo, err := localdir.MakeLocalDirRepo(tt.inputFolder)
if !errors.Is(err, tt.err) {
t.Errorf("MakeLocalDirRepo: %v, expected %v", err, tt.err)
}
ctx := context.Background()
client := localdir.CreateLocalDirClient(ctx, logger)
if err := client.InitRepo(repo); err != nil {
t.Errorf("InitRepo: %v", err)
}
dl := scut.TestDetailLogger{}
req := checker.CheckRequest{
Ctx: ctx,
RepoClient: client,
Dlogger: &dl,
}
res := LicenseCheck(&req)
if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &res, &dl) {
t.Fail()
}
ctrl.Finish()
})
}
}

View File

View File

@ -139,6 +139,10 @@ func getAllChecks() checker.CheckNameToFnMap {
if _, dangerousWorkflowCheck := os.LookupEnv("ENABLE_DANGEROUS_WORKFLOW"); !dangerousWorkflowCheck {
delete(possibleChecks, checks.CheckDangerousWorkflow)
}
// TODO: Remove this to enable the LICENSE_CHECK by default in the next release.
if _, licenseflowCheck := os.LookupEnv("ENABLE_LICENSE_CHECK"); !licenseflowCheck {
delete(possibleChecks, checks.LicenseCheckPolicy)
}
return possibleChecks
}

View File

@ -655,6 +655,7 @@ checks:
- >-
Fix the vulnerabilities. The details of each vulnerability can be found
on <https://osv.dev>.
Dangerous-Workflow:
risk: High
tags: supply-chain, security, infrastructure
@ -682,3 +683,35 @@ checks:
Avoid the dangerous workflow patterns. See this [post](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)
for information on avoiding untrusted code checkouts.
LicenseCheck:
risk: Low
tags: license
repos: GitHub, local
short: Determines if the project has defined a license.
description: |
Risk: `Low` (possible impediment to security review)
This check tries to determine if the project has published a license. It
works by checking standard locations for a file named according to common
conventions for licenses.
A license can give users information about how the source code may or may
not be used. The lack of a license will impede any kind of security review
or audit and creates a legal risk for potential users.
This check will detect files in the top-level directory with any combination
of the following names and extensions:`LICENSE`, `LICENCE`, `COPYING`,
`COPYRIGHT` and .html, .txt, .md. It will also detect these files in a
directory named `LICENSES`. (Files in a `LICENSES` directory are typically
named as their [SPDX](https://spdx.org/licenses/) license identifier followed
by an appropriate file extension, as described in the [REUSE](https://reuse.software/spec/) Specification.)
remediation:
- >-
Determine [which license](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository) to apply to your project.
- >-
Create the license in a .txt, .html, or .md file named LICENSE or COPYING,
and place it in the top-level directory.
- >-
Alternately, create a `LICENSE` directory and add license files with a name
that matches your [SPDX license identifier](https://spdx.dev/ids/).

59
e2e/license_check_test.go Normal file
View File

@ -0,0 +1,59 @@
// 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 e2e
import (
"context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks"
"github.com/ossf/scorecard/v3/clients/githubrepo"
scut "github.com/ossf/scorecard/v3/utests"
)
var _ = Describe("E2E TEST:"+checks.LicenseCheckPolicy, func() {
Context("E2E TEST:Validating license file check", func() {
It("Should return license check works", func() {
dl := scut.TestDetailLogger{}
repo, err := githubrepo.MakeGithubRepo("ossf-tests/scorecard-check-dangerous-workflow-e2e")
Expect(err).Should(BeNil())
repoClient := githubrepo.CreateGithubRepoClient(context.Background(), logger)
err = repoClient.InitRepo(repo)
Expect(err).Should(BeNil())
req := checker.CheckRequest{
Ctx: context.Background(),
RepoClient: repoClient,
Repo: repo,
Dlogger: &dl,
}
expected := scut.TestReturn{
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
}
result := checks.LicenseCheck(&req)
Expect(result.Error).Should(BeNil())
Expect(result.Pass).Should(BeTrue())
Expect(scut.ValidateTestReturn(nil, "license check", &expected, &result,
&dl)).Should(BeTrue())
})
})
})