Skip to content

Commit 64cd053

Browse files
aidenwang9867Aiden Wang
andauthored
✨ Support user-defined fuzz functions (GoLang) in fuzzing check (#1979)
* temp save 05262022 * finished golang fuzz func check, getLang interface to be done next week * temp save 05/31/2022 * temp save 06/01/2022 * temp save-2 06/01/2022 * temp save-1 06032022 * temp save-2 06022022 * temp save * temp save 06032022 * temp save 06032022 (2) * update err def * temp save 3 * update docs for fuzzing * update docs for fuzzing * update checks.yaml to gen docs * temp save 0606 * temp save-2 0606 * temp save-3 0606 * temp save-4 0606 * fix linter errors * fix linter errs-2 * fix e2e errors * 0608 * 0608-2 Co-authored-by: Aiden Wang <aidenwang@google.com>
1 parent 3b7c46f commit 64cd053

File tree

19 files changed

+593
-62
lines changed

19 files changed

+593
-62
lines changed

checker/raw_result.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,10 @@ type BranchProtectionsData struct {
174174

175175
// Tool represents a tool.
176176
type Tool struct {
177-
URL *string
178-
Desc *string
179-
File *File
180-
Name string
177+
URL *string
178+
Desc *string
179+
Files []File
180+
Name string
181181
// Runs of the tool.
182182
Runs []Run
183183
// Issues created by the tool.

checks/evaluation/dependency_update_tool.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,20 @@ func DependencyUpdateTool(name string, dl checker.DetailLogger,
4949
return checker.CreateRuntimeErrorResult(name, e)
5050
}
5151

52-
if r.Tools[0].File == nil {
53-
e := sce.WithMessage(sce.ErrScorecardInternal, "File is nil")
52+
if r.Tools[0].Files == nil {
53+
e := sce.WithMessage(sce.ErrScorecardInternal, "Files are nil")
5454
return checker.CreateRuntimeErrorResult(name, e)
5555
}
5656

57-
// Note: only one file per tool is present,
58-
// so we do not iterate thru all entries.
59-
dl.Info(&checker.LogMessage{
60-
Path: r.Tools[0].File.Path,
61-
Type: r.Tools[0].File.Type,
62-
Offset: r.Tools[0].File.Offset,
63-
Text: fmt.Sprintf("%s detected", r.Tools[0].Name),
64-
})
57+
// Iterate over all the files, since a Tool can contain multiple files.
58+
for _, file := range r.Tools[0].Files {
59+
dl.Info(&checker.LogMessage{
60+
Path: file.Path,
61+
Type: file.Type,
62+
Offset: file.Offset,
63+
Text: fmt.Sprintf("%s detected", r.Tools[0].Name),
64+
})
65+
}
6566

6667
// High score result.
6768
return checker.CreateMaxScoreResult(name, "update tool detected")

checks/evaluation/dependency_update_tool_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,15 @@ func TestDependencyUpdateTool(t *testing.T) {
8888
Tools: []checker.Tool{
8989
{
9090
Name: "DependencyUpdateTool",
91-
File: &checker.File{
92-
Path: "/etc/dependency-update-tool.conf",
93-
Snippet: `
91+
Files: []checker.File{
92+
{
93+
Path: "/etc/dependency-update-tool.conf",
94+
Snippet: `
9495
[dependency-update-tool]
9596
enabled = true
9697
`,
97-
Offset: 0,
98-
Type: 0,
98+
Type: checker.FileTypeSource,
99+
},
99100
},
100101
},
101102
},

checks/evaluation/fuzzing.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,25 @@ func Fuzzing(name string, dl checker.DetailLogger,
3030
return checker.CreateRuntimeErrorResult(name, e)
3131
}
3232

33+
if len(r.Fuzzers) == 0 {
34+
return checker.CreateMinScoreResult(name, "project is not fuzzed")
35+
}
36+
fuzzers := []string{}
3337
for i := range r.Fuzzers {
3438
fuzzer := r.Fuzzers[i]
35-
return checker.CreateMaxScoreResult(name,
36-
fmt.Sprintf("project is fuzzed with %s", fuzzer.Name))
39+
for _, f := range fuzzer.Files {
40+
msg := checker.LogMessage{
41+
Path: f.Path,
42+
Type: f.Type,
43+
Offset: f.Offset,
44+
}
45+
if f.Snippet != "" {
46+
msg.Text = f.Snippet
47+
}
48+
dl.Info(&msg)
49+
}
50+
fuzzers = append(fuzzers, fuzzer.Name)
3751
}
38-
39-
return checker.CreateMinScoreResult(name, "project is not fuzzed")
52+
return checker.CreateMaxScoreResult(name,
53+
fmt.Sprintf("project is fuzzed with %v", fuzzers))
4054
}

checks/fuzzing_test.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func TestFuzzing(t *testing.T) {
3434
tests := []struct {
3535
name string
3636
want checker.CheckResult
37+
langs map[clients.Language]int
3738
response clients.SearchResponse
3839
wantErr bool
3940
wantFuzzErr bool
@@ -44,13 +45,20 @@ func TestFuzzing(t *testing.T) {
4445
{
4546
name: "empty response",
4647
response: clients.SearchResponse{},
47-
wantErr: false,
48+
langs: map[clients.Language]int{
49+
clients.Go: 300,
50+
},
51+
wantErr: false,
4852
},
4953
{
5054
name: "hits 1",
5155
response: clients.SearchResponse{
5256
Hits: 1,
5357
},
58+
langs: map[clients.Language]int{
59+
clients.Go: 100,
60+
clients.Java: 70,
61+
},
5462
wantErr: false,
5563
want: checker.CheckResult{Score: 10},
5664
expected: scut.TestReturn{
@@ -61,7 +69,10 @@ func TestFuzzing(t *testing.T) {
6169
},
6270
},
6371
{
64-
name: "nil response",
72+
name: "nil response",
73+
langs: map[clients.Language]int{
74+
clients.Python: 256,
75+
},
6576
wantErr: true,
6677
want: checker.CheckResult{Score: -1},
6778
expected: scut.TestReturn{
@@ -73,7 +84,15 @@ func TestFuzzing(t *testing.T) {
7384
},
7485
},
7586
{
76-
name: " error",
87+
name: "min score since lang not supported",
88+
langs: map[clients.Language]int{
89+
clients.Language("not_supported_lang"): 1490,
90+
},
91+
wantFuzzErr: false,
92+
want: checker.CheckResult{Score: 0},
93+
},
94+
{
95+
name: "error",
7796
wantFuzzErr: true,
7897
want: checker.CheckResult{},
7998
},
@@ -94,7 +113,7 @@ func TestFuzzing(t *testing.T) {
94113
}
95114
return tt.response, nil
96115
}).AnyTimes()
97-
116+
mockFuzz.EXPECT().ListProgrammingLanguages().Return(tt.langs, nil).AnyTimes()
98117
mockFuzz.EXPECT().ListFiles(gomock.Any()).Return(tt.fileName, nil).AnyTimes()
99118
mockFuzz.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(f string) (string, error) {
100119
if tt.wantErr {

checks/raw/dependency_update_tool.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin
5151
Name: "Dependabot",
5252
URL: asPointer("https://github.com/dependabot"),
5353
Desc: asPointer("Automated dependency updates built into GitHub"),
54-
File: &checker.File{
55-
Path: name,
56-
Type: checker.FileTypeSource,
57-
Offset: checker.OffsetDefault,
54+
Files: []checker.File{
55+
{
56+
Path: name,
57+
Type: checker.FileTypeSource,
58+
Offset: checker.OffsetDefault,
59+
},
5860
},
5961
})
6062

@@ -65,10 +67,12 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin
6567
Name: "Renovabot",
6668
URL: asPointer("https://github.com/renovatebot/renovate"),
6769
Desc: asPointer("Automated dependency updates. Multi-platform and multi-language."),
68-
File: &checker.File{
69-
Path: name,
70-
Type: checker.FileTypeSource,
71-
Offset: checker.OffsetDefault,
70+
Files: []checker.File{
71+
{
72+
Path: name,
73+
Type: checker.FileTypeSource,
74+
Offset: checker.OffsetDefault,
75+
},
7276
},
7377
})
7478
default:

checks/raw/fuzzing.go

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,51 @@
1515
package raw
1616

1717
import (
18+
"bytes"
1819
"fmt"
20+
"regexp"
21+
"strings"
1922

2023
"github.com/ossf/scorecard/v4/checker"
2124
"github.com/ossf/scorecard/v4/checks/fileparser"
2225
"github.com/ossf/scorecard/v4/clients"
2326
sce "github.com/ossf/scorecard/v4/errors"
2427
)
2528

29+
const (
30+
fuzzerOSSFuzz = "OSSFuzz"
31+
fuzzerClusterFuzzLite = "ClusterFuzzLite"
32+
fuzzerBuiltInGo = "GoBuiltInFuzzer"
33+
// TODO: add more fuzzing check supports.
34+
)
35+
36+
type filesWithPatternStr struct {
37+
pattern string
38+
files []checker.File
39+
}
40+
41+
// Configurations for language-specified fuzzers.
42+
type languageFuzzConfig struct {
43+
URL, Desc *string
44+
filePattern, funcPattern, Name string
45+
//TODO: add more language fuzzing-related fields.
46+
}
47+
48+
// Contains fuzzing speficications for programming languages.
49+
// Please use the type Language defined in clients/languages.go rather than a raw string.
50+
var languageFuzzSpecs = map[clients.Language]languageFuzzConfig{
51+
// Default fuzz patterns for Go.
52+
clients.Go: {
53+
filePattern: "*_test.go",
54+
funcPattern: `func\s+Fuzz\w+\s*\(\w+\s+\*testing.F\)`,
55+
Name: fuzzerBuiltInGo,
56+
URL: asPointer("https://go.dev/doc/fuzz/"),
57+
Desc: asPointer(
58+
"Go fuzzing intelligently walks through the source code to report failures and find vulnerabilities."),
59+
},
60+
// TODO: add more language-specific fuzz patterns & configs.
61+
}
62+
2663
// Fuzzing runs Fuzzing check.
2764
func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) {
2865
var fuzzers []checker.Tool
@@ -33,7 +70,7 @@ func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) {
3370
if usingCFLite {
3471
fuzzers = append(fuzzers,
3572
checker.Tool{
36-
Name: "ClusterFuzzLite",
73+
Name: fuzzerClusterFuzzLite,
3774
URL: asPointer("https://github.com/google/clusterfuzzlite"),
3875
Desc: asPointer("continuous fuzzing solution that runs as part of Continuous Integration (CI) workflows"),
3976
// TODO: File.
@@ -48,14 +85,36 @@ func Fuzzing(c *checker.CheckRequest) (checker.FuzzingData, error) {
4885
if usingOSSFuzz {
4986
fuzzers = append(fuzzers,
5087
checker.Tool{
51-
Name: "OSS-Fuzz",
88+
Name: fuzzerOSSFuzz,
5289
URL: asPointer("https://github.com/google/oss-fuzz"),
5390
Desc: asPointer("Continuous Fuzzing for Open Source Software"),
5491
// TODO: File.
5592
},
5693
)
5794
}
5895

96+
langMap, err := c.RepoClient.ListProgrammingLanguages()
97+
if err != nil {
98+
return checker.FuzzingData{}, fmt.Errorf("cannot get langs of repo: %w", err)
99+
}
100+
prominentLangs := getProminentLanguages(langMap)
101+
102+
for _, lang := range prominentLangs {
103+
usingFuzzFunc, files, e := checkFuzzFunc(c, lang)
104+
if e != nil {
105+
return checker.FuzzingData{}, fmt.Errorf("%w", e)
106+
}
107+
if usingFuzzFunc {
108+
fuzzers = append(fuzzers,
109+
checker.Tool{
110+
Name: languageFuzzSpecs[lang].Name,
111+
URL: languageFuzzSpecs[lang].URL,
112+
Desc: languageFuzzSpecs[lang].Desc,
113+
Files: files,
114+
},
115+
)
116+
}
117+
}
59118
return checker.FuzzingData{Fuzzers: fuzzers}, nil
60119
}
61120

@@ -91,3 +150,93 @@ func checkOSSFuzz(c *checker.CheckRequest) (bool, error) {
91150
}
92151
return result.Hits > 0, nil
93152
}
153+
154+
func checkFuzzFunc(c *checker.CheckRequest, lang clients.Language) (bool, []checker.File, error) {
155+
if c.RepoClient == nil {
156+
return false, nil, nil
157+
}
158+
data := filesWithPatternStr{
159+
files: make([]checker.File, 0),
160+
}
161+
// Search language-specified fuzz func patterns in the hashmap.
162+
pattern, found := languageFuzzSpecs[lang]
163+
if !found {
164+
// If the fuzz patterns for the current language not supported yet,
165+
// we return it as false (not found), nil (no files), and nil (no errors).
166+
return false, nil, nil
167+
}
168+
// Get patterns for file and func.
169+
// We use the file pattern in the matcher to match the test files,
170+
// and put the func pattern in var data to match file contents (func names).
171+
filePattern, funcPattern := pattern.filePattern, pattern.funcPattern
172+
matcher := fileparser.PathMatcher{
173+
Pattern: filePattern,
174+
CaseSensitive: false,
175+
}
176+
data.pattern = funcPattern
177+
err := fileparser.OnMatchingFileContentDo(c.RepoClient, matcher, getFuzzFunc, &data)
178+
if err != nil {
179+
return false, nil, fmt.Errorf("error when OnMatchingFileContentDo: %w", err)
180+
}
181+
182+
if len(data.files) == 0 {
183+
// This means no fuzz funcs matched for this language.
184+
return false, nil, nil
185+
}
186+
return true, data.files, nil
187+
}
188+
189+
// This is the callback func for interface OnMatchingFileContentDo
190+
// used for matching fuzz functions in the file content,
191+
// and return a list of files (or nil for not found).
192+
var getFuzzFunc fileparser.DoWhileTrueOnFileContent = func(
193+
path string, content []byte, args ...interface{}) (bool, error) {
194+
if len(args) != 1 {
195+
return false, fmt.Errorf("getFuzzFunc requires exactly one argument: %w", errInvalidArgLength)
196+
}
197+
pdata, ok := args[0].(*filesWithPatternStr)
198+
if !ok {
199+
return false, errInvalidArgType
200+
}
201+
r := regexp.MustCompile(pdata.pattern)
202+
lines := bytes.Split(content, []byte("\n"))
203+
for i, line := range lines {
204+
found := r.FindString(string(line))
205+
if found != "" {
206+
// If fuzz func is found in the file, add it to the file array,
207+
// with its file path as Path, func name as Snippet,
208+
// FileTypeFuzz as Type, and # of lines as Offset.
209+
pdata.files = append(pdata.files, checker.File{
210+
Path: path,
211+
Type: checker.FileTypeSource,
212+
Snippet: found,
213+
Offset: uint(i + 1), // Since the # of lines starts from zero.
214+
})
215+
}
216+
}
217+
return true, nil
218+
}
219+
220+
func getProminentLanguages(langs map[clients.Language]int) []clients.Language {
221+
numLangs := len(langs)
222+
if numLangs == 0 {
223+
return nil
224+
}
225+
totalLoC := 0
226+
for _, LoC := range langs {
227+
totalLoC += LoC
228+
}
229+
// Var avgLoC calculates the average lines of code in the current repo,
230+
// and it can stay as an int, no need for a float value.
231+
avgLoC := totalLoC / numLangs
232+
233+
// Languages that have lines of code above average will be considered prominent.
234+
ret := []clients.Language{}
235+
for lang, LoC := range langs {
236+
if LoC >= avgLoC {
237+
lang = clients.Language(strings.ToLower(string(lang)))
238+
ret = append(ret, lang)
239+
}
240+
}
241+
return ret
242+
}

0 commit comments

Comments
 (0)