Skip to content

Commit e07c630

Browse files
e3n0laurentsimon
authored andcommitted
✨ Binary artifact exception for gradle-wrapper.jar when using validation action (ossf#2039)
* implement binary artifacts exception for validated gradle-wrapper.jar files * add tests for binary artifact gradle wrapper verification exception * fix issues for linter * expect added jar in TestBinaryArtifacts Jar file test * improve readability of raw/binary_artifact * Binary-Artifact request types no longer includes FileBased * add version requirement capability to gradle action check * Refactor exception from checks/raw to checks/evaluation * remove unnecessary len(files) * flatten application of exception by moving to another function * revert refactor to checks/evaluation * flatten removal of validated wrappers * create fileExists function Co-authored-by: laurentsimon <64505099+laurentsimon@users.noreply.github.com>
1 parent 29c0b09 commit e07c630

File tree

12 files changed

+313
-29
lines changed

12 files changed

+313
-29
lines changed

checks/binary_artifact.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const CheckBinaryArtifacts string = "Binary-Artifacts"
2727
//nolint
2828
func init() {
2929
supportedRequestTypes := []checker.RequestType{
30-
checker.FileBased,
3130
checker.CommitBased,
3231
}
3332
if err := registerCheck(CheckBinaryArtifacts, BinaryArtifacts, supportedRequestTypes); err != nil {

checks/binary_artifact_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestBinaryArtifacts(t *testing.T) {
4141
inputFolder: "testdata/binaryartifacts/jars",
4242
err: nil,
4343
expected: checker.CheckResult{
44-
Score: 9,
44+
Score: 8,
4545
},
4646
},
4747
{

checks/raw/binary_artifact.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,35 @@ package raw
1717
import (
1818
"fmt"
1919
"path/filepath"
20+
"regexp"
2021
"strings"
2122
"unicode"
2223

24+
semver "github.com/Masterminds/semver/v3"
2325
"github.com/h2non/filetype"
2426
"github.com/h2non/filetype/types"
27+
"github.com/rhysd/actionlint"
2528

2629
"github.com/ossf/scorecard/v4/checker"
2730
"github.com/ossf/scorecard/v4/checks/fileparser"
2831
"github.com/ossf/scorecard/v4/clients"
2932
sce "github.com/ossf/scorecard/v4/errors"
3033
)
3134

35+
var (
36+
gradleWrapperValidationActionRegex = regexp.MustCompile(`^gradle\/wrapper-validation-action@v?(.+)$`)
37+
gradleWrapperValidationActionVersionConstraint = mustParseConstraint(`>= 1.0.0`)
38+
)
39+
40+
// mustParseConstraint attempts parse of semver constraint, panics if fail.
41+
func mustParseConstraint(c string) *semver.Constraints {
42+
if c, err := semver.NewConstraint(c); err != nil {
43+
panic(fmt.Errorf("failed to parse constraint: %w", err))
44+
} else {
45+
return c
46+
}
47+
}
48+
3249
// BinaryArtifacts retrieves the raw data for the Binary-Artifacts check.
3350
func BinaryArtifacts(c clients.RepoClient) (checker.BinaryArtifactData, error) {
3451
files := []checker.File{}
@@ -39,11 +56,45 @@ func BinaryArtifacts(c clients.RepoClient) (checker.BinaryArtifactData, error) {
3956
if err != nil {
4057
return checker.BinaryArtifactData{}, fmt.Errorf("%w", err)
4158
}
59+
// Ignore validated gradle-wrapper.jar files if present
60+
files, err = excludeValidatedGradleWrappers(c, files)
61+
if err != nil {
62+
return checker.BinaryArtifactData{}, fmt.Errorf("%w", err)
63+
}
4264

4365
// No error, return the files.
4466
return checker.BinaryArtifactData{Files: files}, nil
4567
}
4668

69+
// excludeValidatedGradleWrappers returns the subset of files not confirmed
70+
// to be Action-validated gradle-wrapper.jar files.
71+
func excludeValidatedGradleWrappers(c clients.RepoClient, files []checker.File) ([]checker.File, error) {
72+
// Check if gradle-wrapper.jar present
73+
if !fileExists(files, "gradle-wrapper.jar") {
74+
return files, nil
75+
}
76+
// Gradle wrapper JARs present, so check that they are validated
77+
ok, err := gradleWrapperValidated(c)
78+
if err != nil {
79+
return files, fmt.Errorf(
80+
"failure checking for Gradle wrapper validating Action: %w", err)
81+
}
82+
if !ok {
83+
// Gradle Wrappers not validated
84+
return files, nil
85+
}
86+
// It has been confirmed that latest commit has validated JARs!
87+
// Remove Gradle wrapper JARs from files.
88+
filterFiles := []checker.File{}
89+
for _, f := range files {
90+
if filepath.Base(f.Path) != "gradle-wrapper.jar" {
91+
filterFiles = append(filterFiles, f)
92+
}
93+
}
94+
files = filterFiles
95+
return files, nil
96+
}
97+
4798
var checkBinaryFileContent fileparser.DoWhileTrueOnFileContent = func(path string, content []byte,
4899
args ...interface{},
49100
) (bool, error) {
@@ -127,3 +178,91 @@ func isText(content []byte) bool {
127178
}
128179
return true
129180
}
181+
182+
// gradleWrapperValidated checks for the gradle-wrapper-verify action being
183+
// used in a non-failing workflow on the latest commit.
184+
func gradleWrapperValidated(c clients.RepoClient) (bool, error) {
185+
gradleWrapperValidatingWorkflowFile := ""
186+
err := fileparser.OnMatchingFileContentDo(c, fileparser.PathMatcher{
187+
Pattern: ".github/workflows/*",
188+
CaseSensitive: false,
189+
}, checkWorkflowValidatesGradleWrapper, &gradleWrapperValidatingWorkflowFile)
190+
if err != nil {
191+
return false, fmt.Errorf("%w", err)
192+
}
193+
if gradleWrapperValidatingWorkflowFile != "" {
194+
// If validated, check that latest commit has a relevant successful run
195+
runs, err := c.ListSuccessfulWorkflowRuns(gradleWrapperValidatingWorkflowFile)
196+
if err != nil {
197+
return false, fmt.Errorf("failure listing workflow runs: %w", err)
198+
}
199+
commits, err := c.ListCommits()
200+
if err != nil {
201+
return false, fmt.Errorf("failure listing commits: %w", err)
202+
}
203+
if len(commits) < 1 || len(runs) < 1 {
204+
return false, nil
205+
}
206+
for _, r := range runs {
207+
if *r.HeadSHA == commits[0].SHA {
208+
// Commit has corresponding successful run!
209+
return true, nil
210+
}
211+
}
212+
}
213+
return false, nil
214+
}
215+
216+
// checkWorkflowValidatesGradleWrapper checks that the current workflow file
217+
// is indeed using the gradle/wrapper-validation-action action, else continues.
218+
func checkWorkflowValidatesGradleWrapper(path string, content []byte, args ...interface{}) (bool, error) {
219+
validatingWorkflowFile, ok := args[0].(*string)
220+
if !ok {
221+
return false, fmt.Errorf("checkWorkflowValidatesGradleWrapper expects arg[0] of type *string: %w", errInvalidArgType)
222+
}
223+
224+
action, errs := actionlint.Parse(content)
225+
if len(errs) > 0 {
226+
return true, errs[0]
227+
}
228+
229+
for _, j := range action.Jobs {
230+
for _, s := range j.Steps {
231+
ea, ok := s.Exec.(*actionlint.ExecAction)
232+
if !ok {
233+
continue
234+
}
235+
if ea.Uses == nil {
236+
continue
237+
}
238+
sms := gradleWrapperValidationActionRegex.FindStringSubmatch(ea.Uses.Value)
239+
if len(sms) > 1 {
240+
v, err := semver.NewVersion(sms[1])
241+
if err != nil {
242+
// Couldn't parse version, hopefully another step has
243+
// a correct one.
244+
continue
245+
}
246+
if !gradleWrapperValidationActionVersionConstraint.Check(v) {
247+
// Version out of acceptable range.
248+
continue
249+
}
250+
// OK! This is it.
251+
*validatingWorkflowFile = filepath.Base(path)
252+
return true, nil
253+
}
254+
}
255+
}
256+
return true, nil
257+
}
258+
259+
// fileExists checks if a file of name name exists, including within
260+
// subdirectories.
261+
func fileExists(files []checker.File, name string) bool {
262+
for _, f := range files {
263+
if filepath.Base(f.Path) == name {
264+
return true
265+
}
266+
}
267+
return false
268+
}

checks/raw/binary_artifact_test.go

Lines changed: 130 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,54 +21,149 @@ import (
2121

2222
"github.com/golang/mock/gomock"
2323

24+
"github.com/ossf/scorecard/v4/clients"
2425
mockrepo "github.com/ossf/scorecard/v4/clients/mockclients"
2526
)
2627

28+
func strptr(s string) *string {
29+
return &s
30+
}
31+
2732
// TestBinaryArtifact tests the BinaryArtifact checker.
2833
func TestBinaryArtifacts(t *testing.T) {
2934
t.Parallel()
3035
tests := []struct {
31-
name string
32-
err error
33-
files []string
34-
expect int
36+
name string
37+
err error
38+
files [][]string
39+
successfulWorkflowRuns []clients.WorkflowRun
40+
commits []clients.Commit
41+
getFileContentCount int
42+
expect int
3543
}{
3644
{
3745
name: "Jar file",
3846
err: nil,
39-
files: []string{
40-
"../testdata/binaryartifacts/jars/aws-java-sdk-core-1.11.571.jar",
47+
files: [][]string{
48+
{"../testdata/binaryartifacts/jars/aws-java-sdk-core-1.11.571.jar"},
4149
},
42-
expect: 1,
50+
getFileContentCount: 1,
51+
expect: 1,
4352
},
4453
{
4554
name: "Mach-O ARM64 executable",
4655
err: nil,
47-
files: []string{
48-
"../testdata/binaryartifacts/executables/darwin-arm64-bt",
56+
files: [][]string{
57+
{"../testdata/binaryartifacts/executables/darwin-arm64-bt"},
4958
},
50-
expect: 1,
59+
getFileContentCount: 1,
60+
expect: 1,
5161
},
5262
{
5363
name: "non binary file",
5464
err: nil,
55-
files: []string{
56-
"../testdata/licensedir/withlicense/LICENSE",
65+
files: [][]string{
66+
{"../testdata/licensedir/withlicense/LICENSE"},
5767
},
68+
getFileContentCount: 1,
5869
},
5970
{
6071
name: "non binary file",
6172
err: nil,
62-
files: []string{
63-
"../doesnotexist",
73+
files: [][]string{
74+
{"../doesnotexist"},
6475
},
76+
getFileContentCount: 1,
6577
},
6678
{
6779
name: "printable character .lib",
6880
err: nil,
69-
files: []string{
70-
"../testdata/binaryartifacts/printable.lib",
81+
files: [][]string{
82+
{"../testdata/binaryartifacts/printable.lib"},
83+
},
84+
getFileContentCount: 1,
85+
},
86+
{
87+
name: "gradle-wrapper.jar without verification action",
88+
err: nil,
89+
files: [][]string{
90+
{"../testdata/binaryartifacts/jars/gradle-wrapper.jar"},
91+
{},
92+
},
93+
getFileContentCount: 1,
94+
expect: 1,
95+
},
96+
{
97+
name: "gradle-wrapper.jar with verification action",
98+
err: nil,
99+
files: [][]string{
100+
{"../testdata/binaryartifacts/jars/gradle-wrapper.jar"},
101+
{
102+
"../testdata/binaryartifacts/workflows/nonverify.yml",
103+
"../testdata/binaryartifacts/workflows/verify.yml",
104+
},
105+
},
106+
successfulWorkflowRuns: []clients.WorkflowRun{
107+
{
108+
HeadSHA: strptr("sha-a"),
109+
},
110+
},
111+
commits: []clients.Commit{
112+
{
113+
SHA: "sha-a",
114+
},
115+
{
116+
SHA: "sha-old",
117+
},
118+
},
119+
getFileContentCount: 3,
120+
expect: 0,
121+
},
122+
{
123+
name: "gradle-wrapper.jar with non-verification action",
124+
err: nil,
125+
files: [][]string{
126+
{"../testdata/binaryartifacts/jars/gradle-wrapper.jar"},
127+
{"../testdata/binaryartifacts/workflows/nonverify.yml"},
71128
},
129+
getFileContentCount: 2,
130+
expect: 1,
131+
},
132+
{
133+
name: "gradle-wrapper.jar with verification-failing commit",
134+
err: nil,
135+
files: [][]string{
136+
{"../testdata/binaryartifacts/jars/gradle-wrapper.jar"},
137+
{"../testdata/binaryartifacts/workflows/verify.yml"},
138+
},
139+
successfulWorkflowRuns: []clients.WorkflowRun{
140+
{
141+
HeadSHA: strptr("sha-old"),
142+
},
143+
},
144+
commits: []clients.Commit{
145+
{
146+
SHA: "sha-a",
147+
},
148+
{
149+
SHA: "sha-old",
150+
},
151+
},
152+
getFileContentCount: 2,
153+
expect: 1,
154+
},
155+
{
156+
name: "gradle-wrapper.jar with outdated verification action",
157+
err: nil,
158+
files: [][]string{
159+
{"../testdata/binaryartifacts/jars/gradle-wrapper.jar"},
160+
{
161+
"../testdata/binaryartifacts/workflows/nonverify.yml",
162+
"../testdata/binaryartifacts/workflows/verify-outdated-action.yml",
163+
},
164+
},
165+
getFileContentCount: 3,
166+
expect: 1,
72167
},
73168
}
74169
for _, tt := range tests {
@@ -78,15 +173,25 @@ func TestBinaryArtifacts(t *testing.T) {
78173

79174
ctrl := gomock.NewController(t)
80175
mockRepoClient := mockrepo.NewMockRepoClient(ctrl)
81-
mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil)
82-
mockRepoClient.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(file string) ([]byte, error) {
83-
// This will read the file and return the content
84-
content, err := os.ReadFile(file)
85-
if err != nil {
86-
return content, fmt.Errorf("%w", err)
87-
}
88-
return content, nil
89-
})
176+
for _, files := range tt.files {
177+
mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return(files, nil)
178+
}
179+
for i := 0; i < tt.getFileContentCount; i++ {
180+
mockRepoClient.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(file string) ([]byte, error) {
181+
// This will read the file and return the content
182+
content, err := os.ReadFile(file)
183+
if err != nil {
184+
return content, fmt.Errorf("%w", err)
185+
}
186+
return content, nil
187+
})
188+
}
189+
if tt.successfulWorkflowRuns != nil {
190+
mockRepoClient.EXPECT().ListSuccessfulWorkflowRuns(gomock.Any()).Return(tt.successfulWorkflowRuns, nil)
191+
}
192+
if tt.commits != nil {
193+
mockRepoClient.EXPECT().ListCommits().Return(tt.commits, nil)
194+
}
90195

91196
f, err := BinaryArtifacts(mockRepoClient)
92197

54.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)