diff --git a/checker/raw_result.go b/checker/raw_result.go index 2e930fb4a8b..c044e949a39 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -174,10 +174,10 @@ type BranchProtectionsData struct { // Tool represents a tool. type Tool struct { - URL *string - Desc *string - File *File - Name string + URL *string + Desc *string + Files []File + Name string // Runs of the tool. Runs []Run // Issues created by the tool. diff --git a/checks/evaluation/dependency_update_tool.go b/checks/evaluation/dependency_update_tool.go index d906c2574f5..7f709727e78 100644 --- a/checks/evaluation/dependency_update_tool.go +++ b/checks/evaluation/dependency_update_tool.go @@ -49,19 +49,20 @@ func DependencyUpdateTool(name string, dl checker.DetailLogger, return checker.CreateRuntimeErrorResult(name, e) } - if r.Tools[0].File == nil { - e := sce.WithMessage(sce.ErrScorecardInternal, "File is nil") + if r.Tools[0].Files == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "Files are nil") return checker.CreateRuntimeErrorResult(name, e) } - // Note: only one file per tool is present, - // so we do not iterate thru all entries. - dl.Info(&checker.LogMessage{ - Path: r.Tools[0].File.Path, - Type: r.Tools[0].File.Type, - Offset: r.Tools[0].File.Offset, - Text: fmt.Sprintf("%s detected", r.Tools[0].Name), - }) + // Iterate over all the files, since a Tool can contain multiple files. + for _, file := range r.Tools[0].Files { + dl.Info(&checker.LogMessage{ + Path: file.Path, + Type: file.Type, + Offset: file.Offset, + Text: fmt.Sprintf("%s detected", r.Tools[0].Name), + }) + } // High score result. return checker.CreateMaxScoreResult(name, "update tool detected") diff --git a/checks/evaluation/dependency_update_tool_test.go b/checks/evaluation/dependency_update_tool_test.go index 1155671f3e1..edb9840c1bf 100644 --- a/checks/evaluation/dependency_update_tool_test.go +++ b/checks/evaluation/dependency_update_tool_test.go @@ -88,14 +88,15 @@ func TestDependencyUpdateTool(t *testing.T) { Tools: []checker.Tool{ { Name: "DependencyUpdateTool", - File: &checker.File{ - Path: "/etc/dependency-update-tool.conf", - Snippet: ` + Files: []checker.File{ + { + Path: "/etc/dependency-update-tool.conf", + Snippet: ` [dependency-update-tool] enabled = true `, - Offset: 0, - Type: 0, + Type: checker.FileTypeSource, + }, }, }, }, diff --git a/checks/evaluation/fuzzing.go b/checks/evaluation/fuzzing.go index 6967730924d..32d206a5130 100644 --- a/checks/evaluation/fuzzing.go +++ b/checks/evaluation/fuzzing.go @@ -30,11 +30,25 @@ func Fuzzing(name string, dl checker.DetailLogger, return checker.CreateRuntimeErrorResult(name, e) } + if len(r.Fuzzers) == 0 { + return checker.CreateMinScoreResult(name, "project is not fuzzed") + } + fuzzers := []string{} for i := range r.Fuzzers { fuzzer := r.Fuzzers[i] - return checker.CreateMaxScoreResult(name, - fmt.Sprintf("project is fuzzed with %s", fuzzer.Name)) + for _, f := range fuzzer.Files { + msg := checker.LogMessage{ + Path: f.Path, + Type: f.Type, + Offset: f.Offset, + } + if f.Snippet != "" { + msg.Text = f.Snippet + } + dl.Info(&msg) + } + fuzzers = append(fuzzers, fuzzer.Name) } - - return checker.CreateMinScoreResult(name, "project is not fuzzed") + return checker.CreateMaxScoreResult(name, + fmt.Sprintf("project is fuzzed with %v", fuzzers)) } diff --git a/checks/fuzzing_test.go b/checks/fuzzing_test.go index d22c02de261..7fe1a0a75ab 100644 --- a/checks/fuzzing_test.go +++ b/checks/fuzzing_test.go @@ -34,6 +34,7 @@ func TestFuzzing(t *testing.T) { tests := []struct { name string want checker.CheckResult + langs map[clients.Language]int response clients.SearchResponse wantErr bool wantFuzzErr bool @@ -44,13 +45,20 @@ func TestFuzzing(t *testing.T) { { name: "empty response", response: clients.SearchResponse{}, - wantErr: false, + langs: map[clients.Language]int{ + clients.Go: 300, + }, + wantErr: false, }, { name: "hits 1", response: clients.SearchResponse{ Hits: 1, }, + langs: map[clients.Language]int{ + clients.Go: 100, + clients.Java: 70, + }, wantErr: false, want: checker.CheckResult{Score: 10}, expected: scut.TestReturn{ @@ -61,7 +69,10 @@ func TestFuzzing(t *testing.T) { }, }, { - name: "nil response", + name: "nil response", + langs: map[clients.Language]int{ + clients.Python: 256, + }, wantErr: true, want: checker.CheckResult{Score: -1}, expected: scut.TestReturn{ @@ -73,7 +84,15 @@ func TestFuzzing(t *testing.T) { }, }, { - name: " error", + name: "min score since lang not supported", + langs: map[clients.Language]int{ + clients.Language("not_supported_lang"): 1490, + }, + wantFuzzErr: false, + want: checker.CheckResult{Score: 0}, + }, + { + name: "error", wantFuzzErr: true, want: checker.CheckResult{}, }, @@ -94,7 +113,7 @@ func TestFuzzing(t *testing.T) { } return tt.response, nil }).AnyTimes() - + mockFuzz.EXPECT().ListProgrammingLanguages().Return(tt.langs, nil).AnyTimes() mockFuzz.EXPECT().ListFiles(gomock.Any()).Return(tt.fileName, nil).AnyTimes() mockFuzz.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(f string) (string, error) { if tt.wantErr { diff --git a/checks/raw/dependency_update_tool.go b/checks/raw/dependency_update_tool.go index 3ad79d9bea8..8dbfb230ec8 100644 --- a/checks/raw/dependency_update_tool.go +++ b/checks/raw/dependency_update_tool.go @@ -51,10 +51,12 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin Name: "Dependabot", URL: asPointer("https://github.com/dependabot"), Desc: asPointer("Automated dependency updates built into GitHub"), - File: &checker.File{ - Path: name, - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, + Files: []checker.File{ + { + Path: name, + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + }, }, }) @@ -65,10 +67,12 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin Name: "Renovabot", URL: asPointer("https://github.com/renovatebot/renovate"), Desc: asPointer("Automated dependency updates. Multi-platform and multi-language."), - File: &checker.File{ - Path: name, - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, + Files: []checker.File{ + { + Path: name, + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + }, }, }) default: diff --git a/checks/raw/fuzzing.go b/checks/raw/fuzzing.go index 75218d1162e..848cfbd86b3 100644 --- a/checks/raw/fuzzing.go +++ b/checks/raw/fuzzing.go @@ -15,7 +15,10 @@ package raw import ( + "bytes" "fmt" + "regexp" + "strings" "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/checks/fileparser" @@ -23,6 +26,40 @@ import ( sce "github.com/ossf/scorecard/v4/errors" ) +const ( + fuzzerOSSFuzz = "OSSFuzz" + fuzzerClusterFuzzLite = "ClusterFuzzLite" + fuzzerBuiltInGo = "GoBuiltInFuzzer" + // TODO: add more fuzzing check supports. +) + +type filesWithPatternStr struct { + pattern string + files []checker.File +} + +// Configurations for language-specified fuzzers. +type languageFuzzConfig struct { + URL, Desc *string + filePattern, funcPattern, Name string + //TODO: add more language fuzzing-related fields. +} + +// Contains fuzzing speficications for programming languages. +// Please use the type Language defined in clients/languages.go rather than a raw string. +var languageFuzzSpecs = map[clients.Language]languageFuzzConfig{ + // Default fuzz patterns for Go. + clients.Go: { + filePattern: "*_test.go", + funcPattern: `func\s+Fuzz\w+\s*\(\w+\s+\*testing.F\)`, + Name: fuzzerBuiltInGo, + URL: asPointer("https://go.dev/doc/fuzz/"), + Desc: asPointer( + "Go fuzzing intelligently walks through the source code to report failures and find vulnerabilities."), + }, + // TODO: add more language-specific fuzz patterns & configs. +} + // Fuzzing runs Fuzzing check. func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) { var fuzzers []checker.Tool @@ -33,7 +70,7 @@ func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) { if usingCFLite { fuzzers = append(fuzzers, checker.Tool{ - Name: "ClusterFuzzLite", + Name: fuzzerClusterFuzzLite, URL: asPointer("https://github.com/google/clusterfuzzlite"), Desc: asPointer("continuous fuzzing solution that runs as part of Continuous Integration (CI) workflows"), // TODO: File. @@ -48,7 +85,7 @@ func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) { if usingOSSFuzz { fuzzers = append(fuzzers, checker.Tool{ - Name: "OSS-Fuzz", + Name: fuzzerOSSFuzz, URL: asPointer("https://github.com/google/oss-fuzz"), Desc: asPointer("Continuous Fuzzing for Open Source Software"), // TODO: File. @@ -56,6 +93,28 @@ func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) { ) } + langMap, err := c.RepoClient.ListProgrammingLanguages() + if err != nil { + return checker.FuzzingData{}, fmt.Errorf("cannot get langs of repo: %w", err) + } + prominentLangs := getProminentLanguages(langMap) + + for _, lang := range prominentLangs { + usingFuzzFunc, files, e := checkFuzzFunc(c, lang) + if e != nil { + return checker.FuzzingData{}, fmt.Errorf("%w", e) + } + if usingFuzzFunc { + fuzzers = append(fuzzers, + checker.Tool{ + Name: languageFuzzSpecs[lang].Name, + URL: languageFuzzSpecs[lang].URL, + Desc: languageFuzzSpecs[lang].Desc, + Files: files, + }, + ) + } + } return checker.FuzzingData{Fuzzers: fuzzers}, nil } @@ -91,3 +150,93 @@ func checkOSSFuzz(c *checker.CheckRequest) (bool, error) { } return result.Hits > 0, nil } + +func checkFuzzFunc(c *checker.CheckRequest, lang clients.Language) (bool, []checker.File, error) { + if c.RepoClient == nil { + return false, nil, nil + } + data := filesWithPatternStr{ + files: make([]checker.File, 0), + } + // Search language-specified fuzz func patterns in the hashmap. + pattern, found := languageFuzzSpecs[lang] + if !found { + // If the fuzz patterns for the current language not supported yet, + // we return it as false (not found), nil (no files), and nil (no errors). + return false, nil, nil + } + // Get patterns for file and func. + // We use the file pattern in the matcher to match the test files, + // and put the func pattern in var data to match file contents (func names). + filePattern, funcPattern := pattern.filePattern, pattern.funcPattern + matcher := fileparser.PathMatcher{ + Pattern: filePattern, + CaseSensitive: false, + } + data.pattern = funcPattern + err := fileparser.OnMatchingFileContentDo(c.RepoClient, matcher, getFuzzFunc, &data) + if err != nil { + return false, nil, fmt.Errorf("error when OnMatchingFileContentDo: %w", err) + } + + if len(data.files) == 0 { + // This means no fuzz funcs matched for this language. + return false, nil, nil + } + return true, data.files, nil +} + +// This is the callback func for interface OnMatchingFileContentDo +// used for matching fuzz functions in the file content, +// and return a list of files (or nil for not found). +var getFuzzFunc fileparser.DoWhileTrueOnFileContent = func( + path string, content []byte, args ...interface{}) (bool, error) { + if len(args) != 1 { + return false, fmt.Errorf("getFuzzFunc requires exactly one argument: %w", errInvalidArgLength) + } + pdata, ok := args[0].(*filesWithPatternStr) + if !ok { + return false, errInvalidArgType + } + r := regexp.MustCompile(pdata.pattern) + lines := bytes.Split(content, []byte("\n")) + for i, line := range lines { + found := r.FindString(string(line)) + if found != "" { + // If fuzz func is found in the file, add it to the file array, + // with its file path as Path, func name as Snippet, + // FileTypeFuzz as Type, and # of lines as Offset. + pdata.files = append(pdata.files, checker.File{ + Path: path, + Type: checker.FileTypeSource, + Snippet: found, + Offset: uint(i + 1), // Since the # of lines starts from zero. + }) + } + } + return true, nil +} + +func getProminentLanguages(langs map[clients.Language]int) []clients.Language { + numLangs := len(langs) + if numLangs == 0 { + return nil + } + totalLoC := 0 + for _, LoC := range langs { + totalLoC += LoC + } + // Var avgLoC calculates the average lines of code in the current repo, + // and it can stay as an int, no need for a float value. + avgLoC := totalLoC / numLangs + + // Languages that have lines of code above average will be considered prominent. + ret := []clients.Language{} + for lang, LoC := range langs { + if LoC >= avgLoC { + lang = clients.Language(strings.ToLower(string(lang))) + ret = append(ret, lang) + } + } + return ret +} diff --git a/checks/raw/fuzzing_test.go b/checks/raw/fuzzing_test.go index 5f74812e21b..8fdc9daac41 100644 --- a/checks/raw/fuzzing_test.go +++ b/checks/raw/fuzzing_test.go @@ -16,6 +16,8 @@ package raw import ( "errors" + "path" + "regexp" "testing" "github.com/golang/mock/gomock" @@ -155,3 +157,129 @@ func Test_checkCFLite(t *testing.T) { }) } } + +func Test_fuzzFileAndFuncMatchPattern(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + expectedFileMatch bool + expectedFuncMatch bool + lang clients.Language + fileName string + fileContent string + wantErr bool + }{ + { + name: "Test_fuzzFuncRegex file success & func success", + expectedFileMatch: true, + expectedFuncMatch: true, + lang: "go", + fileName: "FOOoo_fOOff_BaRRR_test.go", + fileContent: `func FuzzSomething (fOo_bAR_1234 *testing.F)`, + wantErr: false, + }, + { + name: "Test_fuzzFuncRegex file success & func failure", + expectedFileMatch: true, + expectedFuncMatch: false, + lang: "go", + fileName: "a_unit_test.go", + fileContent: `func TestSomethingUnitTest (t *testing.T)`, + wantErr: true, + }, + { + name: "Test_fuzzFuncRegex file failure & func failure", + expectedFileMatch: false, + expectedFuncMatch: false, + lang: "go", + fileName: "not_a_fuzz_test_file.go", + fileContent: `func main (t *testing.T)`, + wantErr: true, + }, + { + name: "Test_fuzzFuncRegex not a support language", + expectedFileMatch: false, + expectedFuncMatch: false, + lang: "not_a_supported_one", + fileName: "a_fuzz_test.py", + fileContent: `def NotSupported (foo)`, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + langSpecs, ok := languageFuzzSpecs[tt.lang] + if !ok && !tt.wantErr { + t.Errorf("retrieve supported language error") + } + fileMatchPattern := langSpecs.filePattern + fileMatch, err := path.Match(fileMatchPattern, tt.fileName) + if (fileMatch != tt.expectedFileMatch || err != nil) && !tt.wantErr { + t.Errorf("fileMatch = %v, want %v for %v", fileMatch, tt.expectedFileMatch, tt.name) + } + funcRegexPattern := langSpecs.funcPattern + r := regexp.MustCompile(funcRegexPattern) + found := r.MatchString(tt.fileContent) + if (found != tt.expectedFuncMatch) && !tt.wantErr { + t.Errorf("funcMatch = %v, want %v for %v", fileMatch, tt.expectedFileMatch, tt.name) + } + }) + } +} + +func Test_checkFuzzFunc(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + want bool + wantErr bool + langs map[clients.Language]int + fileName []string + fileContent string + }{ + { + // TODO: more test cases needed. @aidenwang9867 + name: "Test_checkFuzzFunc failure", + want: false, + wantErr: false, + fileName: []string{ + "foo_test.go", + "main.go", + }, + langs: map[clients.Language]int{ + clients.Go: 100, + }, + fileContent: "func TestFoo (t *testing.T)", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mockrepo.NewMockRepoClient(ctrl) + mockClient.EXPECT().ListFiles(gomock.Any()).Return(tt.fileName, nil).AnyTimes() + mockClient.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(f string) (string, error) { + if tt.wantErr { + //nolint + return "", errors.New("error") + } + return tt.fileContent, nil + }).AnyTimes() + req := checker.CheckRequest{ + RepoClient: mockClient, + } + for l := range tt.langs { + got, _, err := checkFuzzFunc(&req, l) + if (got != tt.want || err != nil) && !tt.wantErr { + t.Errorf("checkFuzzFunc() = %v, want %v for %v", got, tt.want, tt.name) + } + } + }) + } +} diff --git a/clients/githubrepo/client.go b/clients/githubrepo/client.go index 4dbb8894bae..59d21a0ec4a 100644 --- a/clients/githubrepo/client.go +++ b/clients/githubrepo/client.go @@ -49,6 +49,7 @@ type Client struct { statuses *statusesHandler search *searchHandler webhook *webhookHandler + languages *languagesHandler ctx context.Context tarball tarballHandler } @@ -104,6 +105,8 @@ func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string) error { // Setup webhookHandler. client.webhook.init(client.ctx, client.repourl) + // Setup languagesHandler. + client.languages.init(client.ctx, client.repourl) return nil } @@ -177,6 +180,11 @@ func (client *Client) ListStatuses(ref string) ([]clients.Status, error) { return client.statuses.listStatuses(ref) } +//ListProgrammingLanguages implements RepoClient.ListProgrammingLanguages. +func (client *Client) ListProgrammingLanguages() (map[clients.Language]int, error) { + return client.languages.listProgrammingLanguages() +} + // Search implements RepoClient.Search. func (client *Client) Search(request clients.SearchRequest) (clients.SearchResponse, error) { return client.search.search(request) @@ -226,6 +234,9 @@ func CreateGithubRepoClientWithTransport(ctx context.Context, rt http.RoundTripp webhook: &webhookHandler{ ghClient: client, }, + languages: &languagesHandler{ + ghclient: client, + }, } } diff --git a/clients/githubrepo/languages.go b/clients/githubrepo/languages.go new file mode 100644 index 00000000000..24793a7c882 --- /dev/null +++ b/clients/githubrepo/languages.go @@ -0,0 +1,74 @@ +// 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 githubrepo + +import ( + "context" + "fmt" + "path" + "sync" + + "github.com/google/go-github/v38/github" + + "github.com/ossf/scorecard/v4/clients" +) + +type languagesHandler struct { + ghclient *github.Client + once *sync.Once + ctx context.Context + errSetup error + repourl *repoURL + languages map[clients.Language]int +} + +func (handler *languagesHandler) init(ctx context.Context, repourl *repoURL) { + handler.ctx = ctx + handler.repourl = repourl + handler.errSetup = nil + handler.once = new(sync.Once) +} + +// TODO: Can add support to parse the raw reponse JSON and mark languages that are not in +// our defined Language consts in clients/languages.go as "not supported languages". +func (handler *languagesHandler) setup() error { + handler.once.Do(func() { + client := handler.ghclient + reqURL := path.Join("repos", handler.repourl.owner, handler.repourl.repo, "languages") + req, err := client.NewRequest("GET", reqURL, nil) + if err != nil { + handler.errSetup = fmt.Errorf("request for repo languages failed with %w", err) + return + } + handler.languages = map[clients.Language]int{} + // The client.repoClient.Do API writes the reponse body to the handler.languages, + // so we can ignore the first returned variable (the entire http response object) + // since we only need the response body here. + _, err = client.Do(handler.ctx, req, &handler.languages) + if err != nil { + handler.errSetup = fmt.Errorf("response for repo languages failed with %w", err) + return + } + handler.errSetup = nil + }) + return handler.errSetup +} + +func (handler *languagesHandler) listProgrammingLanguages() (map[clients.Language]int, error) { + if err := handler.setup(); err != nil { + return nil, fmt.Errorf("error during languagesHandler.setup: %w", err) + } + return handler.languages, nil +} diff --git a/clients/languages.go b/clients/languages.go new file mode 100644 index 00000000000..cc41ede9632 --- /dev/null +++ b/clients/languages.go @@ -0,0 +1,73 @@ +// 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 clients + +// Language represents a customized string for languages used by clients. +// A language could be a programming language, or more general, +// such as Dockerfile, CMake, HTML, YAML, etc. +type Language string + +// TODO: retrieve all languages supported by GitHub. +const ( + // Go: https://go.dev/ + Go Language = "go" + + // Python: https://www.python.org/ + Python Language = "python" + + // JavaScript: https://www.javascript.com/ + JavaScript Language = "javascript" + + // C++: https://cplusplus.com/ + Cpp Language = "c++" + + // C: https://www.open-std.org/jtc1/sc22/wg14/ + C Language = "c" + + // TypeScript: https://www.typescriptlang.org/ + TypeScript Language = "typescript" + + // Java: https://www.java.com/en/ + Java Language = "java" + + // C#: https://docs.microsoft.com/en-us/dotnet/csharp/ + CSharp Language = "c#" + + // Ruby: https://www.ruby-lang.org/ + Ruby Language = "ruby" + + // PHP: https://www.php.net/ + PHP Language = "php" + + // Starlark: https://github.com/bazelbuild/starlark + StarLark Language = "starlark" + + // Scala: https://www.scala-lang.org/ + Scala Language = "scala" + + // Kotlin: https://kotlinlang.org/ + Kotlin Language = "kotlin" + + // Swift: https://github.com/apple/swift + Swift Language = "swift" + + // Rust: https://github.com/rust-lang/rust + Rust Language = "rust" + + // Other indicates other programming languages not listed by the GitHub API. + Other Language = "other" + + // Add more programming languages here if needed, please use lower cases. +) diff --git a/clients/localdir/client.go b/clients/localdir/client.go index 158161662d5..7a97df61614 100644 --- a/clients/localdir/client.go +++ b/clients/localdir/client.go @@ -218,6 +218,12 @@ func (client *localDirClient) Close() error { return nil } +// ListProgrammingLanguages implements RepoClient.ListProgrammingLanguages. +// TODO: add ListProgrammingLanguages support for local directories +func (client *localDirClient) ListProgrammingLanguages() (map[clients.Language]int, error) { + return nil, fmt.Errorf("ListProgrammingLanguages: %w", clients.ErrUnsupportedFeature) +} + // CreateLocalDirClient returns a client which implements RepoClient interface. func CreateLocalDirClient(ctx context.Context, logger *log.Logger) clients.RepoClient { return &localDirClient{ diff --git a/clients/mockclients/repo_client.go b/clients/mockclients/repo_client.go index 71bc7cbd399..4036d136d72 100644 --- a/clients/mockclients/repo_client.go +++ b/clients/mockclients/repo_client.go @@ -272,6 +272,20 @@ func (mr *MockRepoClientMockRecorder) ListWebhooks() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWebhooks", reflect.TypeOf((*MockRepoClient)(nil).ListWebhooks)) } +func (m *MockRepoClient) ListProgrammingLanguages() (map[clients.Language]int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProgrammingLanguages") + ret0, _ := ret[0].(map[clients.Language]int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListProgrammingLanguages indicates an expected call of ListProgrammingLanguages. +func (mr *MockRepoClientMockRecorder) ListProgrammingLanguages() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProgrammingLanguages", reflect.TypeOf((*MockRepoClient)(nil).ListProgrammingLanguages)) +} + // Search mocks base method. func (m *MockRepoClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) { m.ctrl.T.Helper() diff --git a/clients/repo_client.go b/clients/repo_client.go index 78f51c94918..739330dcf7a 100644 --- a/clients/repo_client.go +++ b/clients/repo_client.go @@ -42,6 +42,7 @@ type RepoClient interface { ListCheckRunsForRef(ref string) ([]CheckRun, error) ListStatuses(ref string) ([]Status, error) ListWebhooks() ([]Webhook, error) + ListProgrammingLanguages() (map[Language]int, error) Search(request SearchRequest) (SearchResponse, error) Close() error } diff --git a/cron/internal/format/json_raw_results.go b/cron/internal/format/json_raw_results.go index e863c022b5a..f6221445c52 100644 --- a/cron/internal/format/json_raw_results.go +++ b/cron/internal/format/json_raw_results.go @@ -40,10 +40,10 @@ type jsonFile struct { } type jsonTool struct { - URL *string `json:"url"` - Desc *string `json:"desc"` - File *jsonFile `json:"file"` - Name string `json:"name"` + URL *string `json:"url"` + Desc *string `json:"desc"` + Name string `json:"name"` + Files []jsonFile `json:"file"` // TODO: Runs, Issues, Merge requests. } @@ -204,9 +204,11 @@ func addDependencyUpdateToolRawResults(r *jsonScorecardRawResult, URL: t.URL, Desc: t.Desc, } - if t.File != nil { - jt.File = &jsonFile{ - Path: t.File.Path, + if t.Files != nil && len(t.Files) > 0 { + for _, f := range t.Files { + jt.Files = append(jt.Files, jsonFile{ + Path: f.Path, + }) } } r.Results.DependencyUpdateTools = append(r.Results.DependencyUpdateTools, jt) diff --git a/docs/checks.md b/docs/checks.md index 4f9e0dcccc4..70696ed1517 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -317,9 +317,10 @@ low score is therefore not a definitive indication that the project is at risk. Risk: `Medium` (possible vulnerabilities in code) This check tries to determine if the project uses -[fuzzing](https://owasp.org/www-community/Fuzzing) by checking if the repository -name is included in the [OSS-Fuzz](https://github.com/google/oss-fuzz) project -list. +[fuzzing](https://owasp.org/www-community/Fuzzing) by checking: +1. if the repository name is included in the [OSS-Fuzz](https://github.com/google/oss-fuzz) project list; +2. if [ClusterFuzzLite](https://google.github.io/clusterfuzzlite/) is deployed in the repository; +3. if there are user-defined language-specified fuzzing functions (currently only supports [Go fuzzing](https://go.dev/doc/fuzz/)) in the repository. Fuzzing, or fuzz testing, is the practice of feeding unexpected or random data into a program to expose bugs. Regular fuzzing is important to detect @@ -469,7 +470,7 @@ dependencies using the [GitHub dependency graph](https://docs.github.com/en/code - First determine if your project is producing a library or application. If it is a library, you generally don't want to pin dependencies of library users, and should not follow any remediation steps. - If your project is producing an application, declare all your dependencies with specific versions in your package format file (e.g. `package.json` for npm, `requirements.txt` for python). For C/C++, check in the code from a trusted source and add a `README` on the specific version used (and the archive SHA hashes). - If the package manager supports lock files (e.g. `package-lock.json` for npm), make sure to check these in the source code as well. These files maintain signatures for the entire dependency tree and saves from future exploitation in case the package is compromised. -- For Dockerfiles, pin dependencies by hash. See [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) for example. If you are using a manifest list to support builds across multiple architectures, you can pin to the manifest list hash instead of a single image hash. You can use a tool like [crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md) to obtain the hash of the manifest list like in this [example](https://github.com/ossf/scorecard/issues/1773#issuecomment-1076699039). +- For Dockerfiles, pin dependencies by hash. See [Dockerfile](https://github.com/ossf/scorecard/blob/main/cron/worker/Dockerfile) for example. If you are using a manifest list to support builds across multiple architectures, you can pin to the manifest list hash instead of a single image hash. You can use a tool like [crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md) to obtain the hash of the manifest list like in this [example](https://github.com/ossf/scorecard/issues/1773#issuecomment-1076699039). - For GitHub workflows, pin dependencies by hash. See [main.yaml](https://github.com/ossf/scorecard/blob/f55b86d6627cc3717e3a0395e03305e81b9a09be/.github/workflows/main.yml#L27) for example. To determine the permissions needed for your workflows, you may use [StepSecurity's online tool](https://app.stepsecurity.io/) by ticking the "Pin actions to a full length commit SHA". You may also tick the "Restrict permissions for GITHUB_TOKEN" to fix issues found by the Token-Permissions check. - To help update your dependencies after pinning them, use tools such as Github's [dependabot](https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/) diff --git a/docs/checks/internal/checks.yaml b/docs/checks/internal/checks.yaml index 73dcdd20950..f15bd2f9e93 100644 --- a/docs/checks/internal/checks.yaml +++ b/docs/checks/internal/checks.yaml @@ -374,9 +374,10 @@ checks: Risk: `Medium` (possible vulnerabilities in code) This check tries to determine if the project uses - [fuzzing](https://owasp.org/www-community/Fuzzing) by checking if the repository - name is included in the [OSS-Fuzz](https://github.com/google/oss-fuzz) project - list. + [fuzzing](https://owasp.org/www-community/Fuzzing) by checking: + 1. if the repository name is included in the [OSS-Fuzz](https://github.com/google/oss-fuzz) project list; + 2. if [ClusterFuzzLite](https://google.github.io/clusterfuzzlite/) is deployed in the repository; + 3. if there are user-defined language-specified fuzzing functions (currently only supports [Go fuzzing](https://go.dev/doc/fuzz/)) in the repository. Fuzzing, or fuzz testing, is the practice of feeding unexpected or random data into a program to expose bugs. Regular fuzzing is important to detect diff --git a/e2e/fuzzing_test.go b/e2e/fuzzing_test.go index efe9917b79b..15c6def89e1 100644 --- a/e2e/fuzzing_test.go +++ b/e2e/fuzzing_test.go @@ -85,6 +85,34 @@ var _ = Describe("E2E TEST:"+checks.CheckFuzzing, func() { Expect(repoClient.Close()).Should(BeNil()) Expect(ossFuzzRepoClient.Close()).Should(BeNil()) }) + It("Should return use of GoBuiltInFuzzers", func() { + dl := scut.TestDetailLogger{} + repo, err := githubrepo.MakeGithubRepo("ossf-tests/scorecard-check-fuzzing-golang") + Expect(err).Should(BeNil()) + repoClient := githubrepo.CreateGithubRepoClient(context.Background(), logger) + err = repoClient.InitRepo(repo, clients.HeadSHA) + Expect(err).Should(BeNil()) + ossFuzzRepoClient, err := githubrepo.CreateOssFuzzRepoClient(context.Background(), logger) + Expect(err).Should(BeNil()) + req := checker.CheckRequest{ + Ctx: context.Background(), + RepoClient: repoClient, + OssFuzzRepo: ossFuzzRepoClient, + Repo: repo, + Dlogger: &dl, + } + expected := scut.TestReturn{ + Error: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 2, + NumberOfDebug: 0, + } + result := checks.Fuzzing(&req) + Expect(scut.ValidateTestReturn(nil, "use fuzzing", &expected, &result, &dl)).Should(BeTrue()) + Expect(repoClient.Close()).Should(BeNil()) + Expect(ossFuzzRepoClient.Close()).Should(BeNil()) + }) It("Should return no fuzzing", func() { dl := scut.TestDetailLogger{} repo, err := githubrepo.MakeGithubRepo("ossf-tests/scorecard-check-packaging-e2e") diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index 2dcc354c477..9163f79e6be 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -47,11 +47,11 @@ type jsonFile struct { } type jsonTool struct { - URL *string `json:"url"` - Desc *string `json:"desc"` - Job *jsonWorkflowJob `json:"job,omitempty"` - File *jsonFile `json:"file,omitempty"` - Name string `json:"name"` + URL *string `json:"url"` + Desc *string `json:"desc"` + Job *jsonWorkflowJob `json:"job,omitempty"` + Name string `json:"name"` + Files []jsonFile `json:"files,omitempty"` // TODO: Runs, Issues, Merge requests. } @@ -553,9 +553,11 @@ func (r *jsonScorecardRawResult) addFuzzingRawResults(fd *checker.FuzzingData) e URL: f.URL, Desc: f.Desc, } - if f.File != nil { - jt.File = &jsonFile{ - Path: f.File.Path, + if f.Files != nil { + for _, f := range f.Files { + jt.Files = append(jt.Files, jsonFile{ + Path: f.Path, + }) } } r.Results.Fuzzers = append(r.Results.Fuzzers, jt) @@ -573,9 +575,11 @@ func (r *jsonScorecardRawResult) addDependencyUpdateToolRawResults(dut *checker. URL: t.URL, Desc: t.Desc, } - if t.File != nil { - jt.File = &jsonFile{ - Path: t.File.Path, + if t.Files != nil { + for _, f := range t.Files { + jt.Files = append(jt.Files, jsonFile{ + Path: f.Path, + }) } } r.Results.DependencyUpdateTools = append(r.Results.DependencyUpdateTools, jt)