mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-17 11:57:12 +03:00
✨ 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:
parent
8cb4804c28
commit
45b5a35020
147
checks/license_check.go
Normal file
147
checks/license_check.go
Normal 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
|
||||
}
|
184
checks/license_check_test.go
Normal file
184
checks/license_check_test.go
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
0
checks/licensedir/withlicense/LICENSE
Normal file
0
checks/licensedir/withlicense/LICENSE
Normal file
0
checks/licensedir/withoutlicense/README.md
Normal file
0
checks/licensedir/withoutlicense/README.md
Normal 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
|
||||
}
|
||||
|
||||
|
@ -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
59
e2e/license_check_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user