cleanup Frozen-Deps MakeResultAnd (#742)

* draft

* fixes

* commi 1

* delete file

* clean

* clean 2

* linter

* fix score

* handle err

* in-proress score

* fixes
This commit is contained in:
laurentsimon 2021-07-26 15:02:46 -07:00 committed by GitHub
parent 8128f9fe68
commit a004ffb107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 318 additions and 356 deletions

View File

@ -98,24 +98,24 @@ func CreateProportionalScore(b, t int) int {
// and normalizes the result.
// Each score contributes equally.
func AggregateScores(scores ...int) int {
n := len(scores)
r := float32(0)
n := float64(len(scores))
r := 0
for _, s := range scores {
r += float32(s) / float32(MaxResultScore) * float32(n)
r += s
}
return int(r)
return int(math.Floor(float64(r) / n))
}
// AggregateScoresWithWeight adds up all scores
// and normalizes the result.
// The caller is responsible for ensuring the sum of
// weights is 10.
func AggregateScoresWithWeight(scores map[int]int) int {
r := float32(0)
r := 0
ws := 0
for s, w := range scores {
r += float32(s) / float32(MaxResultScore) * float32(w)
r += s * w
ws += w
}
return int(r)
return int(math.Floor(float64(r) / float64(ws)))
}
func NormalizeReason(reason string, score int) string {

View File

@ -132,17 +132,6 @@ func MultiCheckOr2(fns ...CheckFn) CheckFn {
}
}
func MultiCheckAnd2(fns ...CheckFn) CheckFn {
return func(c *CheckRequest) CheckResult {
var checks []CheckResult
for _, fn := range fns {
res := fn(c)
checks = append(checks, res)
}
return MakeAndResult2(checks...)
}
}
// UPGRADEv2: will be removed.
// MultiCheckOr returns the best check result out of several ones performed.
func MultiCheckOr(fns ...CheckFn) CheckFn {
@ -164,16 +153,3 @@ func MultiCheckOr(fns ...CheckFn) CheckFn {
return maxResult
}
}
// MultiCheckAnd means all checks must succeed. This returns a conservative result
// where the worst result is returned.
func MultiCheckAnd(fns ...CheckFn) CheckFn {
return func(c *CheckRequest) CheckResult {
var checks []CheckResult
for _, fn := range fns {
res := fn(c)
checks = append(checks, res)
}
return MakeAndResult(checks...)
}
}

View File

@ -1,151 +0,0 @@
// 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 checker
import (
"errors"
"testing"
)
const checkTest = "Check-Test"
var errorTest = errors.New("test error")
func TestMakeCheckAnd(t *testing.T) {
t.Parallel()
tests := []struct {
name string
want CheckResult
checks []CheckResult
}{
{
name: "Multiple passing",
checks: []CheckResult{
{
Name: checkTest,
Pass: true,
Details: nil,
Confidence: 5,
ShouldRetry: false,
Error: errorTest,
},
{
Name: checkTest,
Pass: true,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: errorTest,
},
},
want: CheckResult{
Name: checkTest,
Pass: true,
Details: nil,
Confidence: 5,
ShouldRetry: false,
Error: nil,
},
},
{
name: "Multiple failing",
checks: []CheckResult{
{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: errorTest,
},
{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 5,
ShouldRetry: false,
Error: errorTest,
},
},
want: CheckResult{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: errorTest,
},
},
{
name: "Passing and failing",
checks: []CheckResult{
{
Name: checkTest,
Pass: true,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: nil,
},
{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 5,
ShouldRetry: false,
Error: errorTest,
},
{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: errorTest,
},
},
want: CheckResult{
Name: checkTest,
Pass: false,
Details: nil,
Confidence: 10,
ShouldRetry: false,
Error: errorTest,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := MakeAndResult(tt.checks...)
if result.Pass != tt.want.Pass || result.Confidence != tt.want.Confidence {
t.Errorf("MakeAndResult failed (%s): got %v, expected %v", tt.name, result, tt.want)
}
// Also test CheckFn variant
var fns []CheckFn
for _, c := range tt.checks {
check := c
fns = append(fns, func(*CheckRequest) CheckResult { return check })
}
c := CheckRequest{}
resultfn := MultiCheckAnd(fns...)(&c)
if resultfn.Pass != tt.want.Pass || resultfn.Confidence != tt.want.Confidence {
t.Errorf("MultiCheckAnd failed (%s): got %v, expected %v", tt.name, resultfn, tt.want)
}
})
}
}

View File

@ -75,10 +75,13 @@ func CIIBestPractices(c *checker.CheckRequest) checker.CheckResult {
// https://bestpractices.coreinfrastructure.org/en/criteria.
const silverScore = 7
const passingScore = 5
const inProgressScore = 2
switch {
default:
e := sce.Create(sce.ErrScorecardInternal, "unsupported badge")
e := sce.Create(sce.ErrScorecardInternal, fmt.Sprintf("unsupported badge: %v", result.BadgeLevel))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
case strings.Contains(result.BadgeLevel, "in_progress"):
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: in_progress", inProgressScore)
case strings.Contains(result.BadgeLevel, "silver"):
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: silver", silverScore)
case strings.Contains(result.BadgeLevel, "gold"):

View File

@ -59,40 +59,94 @@ func init() {
// FrozenDeps will check the repository if it contains frozen dependecies.
func FrozenDeps(c *checker.CheckRequest) checker.CheckResult {
return checker.MultiCheckAnd2(
isPackageManagerLockFilePresent,
isGitHubActionsWorkflowPinned,
isDockerfilePinned,
isDockerfileFreeOfInsecureDownloads,
isShellScriptFreeOfInsecureDownloads,
isGitHubWorkflowScriptFreeOfInsecureDownloads,
)(c)
// Lock file.
lockScore, lockErr := isPackageManagerLockFilePresent(c)
if lockErr != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, lockErr)
}
// GitHub actions.
actionScore, actionErr := isGitHubActionsWorkflowPinned(c)
if actionErr != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, actionErr)
}
// Docker files.
dockerFromScore, dockerFromErr := isDockerfilePinned(c)
if dockerFromErr != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, dockerFromErr)
}
// Docker downloads.
dockerDownloadScore, dockerDownloadErr := isDockerfileFreeOfInsecureDownloads(c)
if dockerDownloadErr != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, dockerDownloadErr)
}
// Script downloads.
scriptScore, scriptError := isShellScriptFreeOfInsecureDownloads(c)
if scriptError != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, scriptError)
}
// Action script downloads.
actionScriptScore, actionScriptError := isGitHubWorkflowScriptFreeOfInsecureDownloads(c)
if actionScriptError != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, actionScriptError)
}
// Scores may be inconclusive.
lockScore = maxScore(0, lockScore)
actionScore = maxScore(0, actionScore)
dockerFromScore = maxScore(0, dockerFromScore)
dockerDownloadScore = maxScore(0, dockerDownloadScore)
scriptScore = maxScore(0, scriptScore)
actionScriptScore = maxScore(0, actionScriptScore)
score := checker.AggregateScores(lockScore, actionScore, dockerFromScore,
dockerDownloadScore, scriptScore, actionScriptScore)
if score == checker.MaxResultScore {
checker.CreateMaxScoreResult(CheckFrozenDeps, "all dependencies are pinned")
}
return checker.CreateProportionalScoreResult(CheckFrozenDeps,
"unpinned dependencies detected", score, checker.MaxResultScore)
}
// TODO(laurent): need to support GCB pinning.
func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult {
r, err := CheckFilesContent("*", false, c, validateShellScriptIsFreeOfInsecureDownloads)
return createResultForIsShellScriptFreeOfInsecureDownloads(r, err)
//nolint
func maxScore(s1, s2 int) int {
if s1 > s2 {
return s1
}
return s2
}
func createResultForIsShellScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult {
func createReturnValues(r bool, infoMsg string, dl checker.DetailLogger, err error) (int, error) {
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
return checker.InconclusiveResultScore, err
}
if !r {
return checker.CreateMinScoreResult(CheckFrozenDeps,
"insecure (unpinned) dependency downloads found in shell scripts")
return checker.MinResultScore, nil
}
return checker.CreateMaxScoreResult(CheckFrozenDeps,
"no insecure (unpinned) dependency downloads found in shell scripts")
dl.Info(infoMsg)
return checker.MaxResultScore, nil
}
func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
r, err := CheckFilesContent("*", false, c, validateShellScriptIsFreeOfInsecureDownloads)
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
}
func createReturnForIsShellScriptFreeOfInsecureDownloads(r bool, dl checker.DetailLogger, err error) (int, error) {
return createReturnValues(r,
"no insecure (unpinned) dependency downloads found in shell scripts",
dl, err)
}
func testValidateShellScriptIsFreeOfInsecureDownloads(pathfn string,
content []byte, dl checker.DetailLogger) checker.CheckResult {
content []byte, dl checker.DetailLogger) (int, error) {
r, err := validateShellScriptIsFreeOfInsecureDownloads(pathfn, content, dl)
return createResultForIsShellScriptFreeOfInsecureDownloads(r, err)
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, dl, err)
}
func validateShellScriptIsFreeOfInsecureDownloads(pathfn string, content []byte,
@ -104,33 +158,30 @@ func validateShellScriptIsFreeOfInsecureDownloads(pathfn string, content []byte,
return validateShellFile(pathfn, content, dl)
}
func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult {
func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
r, err := CheckFilesContent("*Dockerfile*", false, c, validateDockerfileIsFreeOfInsecureDownloads)
return createResultForIsDockerfileFreeOfInsecureDownloads(r, err)
return createReturnForIsDockerfileFreeOfInsecureDownloads(r, c.Dlogger, err)
}
// Create the result.
func createResultForIsDockerfileFreeOfInsecureDownloads(r bool, err error) checker.CheckResult {
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
}
if !r {
return checker.CreateMinScoreResult(CheckFrozenDeps,
"insecure (unpinned) dependency downloads found in Dockerfiles")
}
return checker.CreateMaxScoreResult(CheckFrozenDeps,
"no insecure (unpinned) dependency downloads found in Dockerfiles")
func createReturnForIsDockerfileFreeOfInsecureDownloads(r bool, dl checker.DetailLogger, err error) (int, error) {
return createReturnValues(r,
"no insecure (unpinned) dependency downloads found in Dockerfiles",
dl, err)
}
func testValidateDockerfileIsFreeOfInsecureDownloads(pathfn string,
content []byte, dl checker.DetailLogger) checker.CheckResult {
content []byte, dl checker.DetailLogger) (int, error) {
r, err := validateDockerfileIsFreeOfInsecureDownloads(pathfn, content, dl)
return createResultForIsDockerfileFreeOfInsecureDownloads(r, err)
return createReturnForIsDockerfileFreeOfInsecureDownloads(r, dl, err)
}
func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte,
dl checker.DetailLogger) (bool, error) {
// Return early if this is a script, e.g. script_dockerfile_something.sh
if isShellScriptFile(pathfn, content) {
return true, nil
}
contentReader := strings.NewReader(string(content))
res, err := parser.Parse(contentReader)
if err != nil {
@ -167,26 +218,21 @@ func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte,
return validateShellFile(pathfn, bytes, dl)
}
func isDockerfilePinned(c *checker.CheckRequest) checker.CheckResult {
func isDockerfilePinned(c *checker.CheckRequest) (int, error) {
r, err := CheckFilesContent("*Dockerfile*", false, c, validateDockerfileIsPinned)
return createResultForIsDockerfilePinned(r, err)
return createReturnForIsDockerfilePinned(r, c.Dlogger, err)
}
// Create the result.
func createResultForIsDockerfilePinned(r bool, err error) checker.CheckResult {
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
}
if r {
return checker.CreateMaxScoreResult(CheckFrozenDeps, "Dockerfile dependencies are pinned")
}
return checker.CreateMinScoreResult(CheckFrozenDeps, "unpinned dependencies found Dockerfiles")
func createReturnForIsDockerfilePinned(r bool, dl checker.DetailLogger, err error) (int, error) {
return createReturnValues(r,
"Dockerfile dependencies are pinned",
dl, err)
}
func testValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult {
func testValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) (int, error) {
r, err := validateDockerfileIsPinned(pathfn, content, dl)
return createResultForIsDockerfilePinned(r, err)
return createReturnForIsDockerfilePinned(r, dl, err)
}
func validateDockerfileIsPinned(pathfn string, content []byte,
@ -195,13 +241,17 @@ func validateDockerfileIsPinned(pathfn string, content []byte,
// Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template
// Templates may trigger false positives, e.g. FROM { NAME }.
// Return early if this is a script, e.g. script_dockerfile_something.sh
if isShellScriptFile(pathfn, content) {
return true, nil
}
// We have what looks like a docker file.
// Let's interpret the content as utf8-encoded strings.
contentReader := strings.NewReader(string(content))
regex := regexp.MustCompile(`.*@sha256:[a-f\d]{64}`)
ret := true
fromFound := false
pinnedAsNames := make(map[string]bool)
res, err := parser.Parse(contentReader)
if err != nil {
@ -215,9 +265,6 @@ func validateDockerfileIsPinned(pathfn string, content []byte,
continue
}
// New 'FROM' line found.
fromFound = true
var valueList []string
for n := child.Next; n != nil; n = n.Next {
valueList = append(valueList, n.Value)
@ -261,38 +308,30 @@ func validateDockerfileIsPinned(pathfn string, content []byte,
}
}
// The file should have at least one FROM statement.
if !fromFound {
//nolint
return false, sce.Create(sce.ErrScorecardInternal, errInternalInvalidDockerFile.Error())
}
//nolint
// The file need not have a FROM statement,
// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/dockerfiles/partials/jupyter.partial.Dockerfile.
return ret, nil
}
func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) checker.CheckResult {
func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
r, err := CheckFilesContent(".github/workflows/*", false, c, validateGitHubWorkflowIsFreeOfInsecureDownloads)
return createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, err)
return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
}
// Create the result.
func createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool, err error) checker.CheckResult {
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
}
if !r {
return checker.CreateMinScoreResult(CheckFrozenDeps,
"insecure (unpinned) dependency downloads found in GitHub workflows")
}
return checker.CreateMaxScoreResult(CheckFrozenDeps,
"no insecure (unpinned) dependency downloads found in GitHub workflows")
func createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r bool,
dl checker.DetailLogger, err error) (int, error) {
return createReturnValues(r,
"no insecure (unpinned) dependency downloads found in GitHub workflows",
dl, err)
}
func testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string,
content []byte, dl checker.DetailLogger) checker.CheckResult {
content []byte, dl checker.DetailLogger) (int, error) {
r, err := validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn, content, dl)
return createResultForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, err)
return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, dl, err)
}
func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte,
@ -354,26 +393,21 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
}
// Check pinning of github actions in workflows.
func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) checker.CheckResult {
func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) {
r, err := CheckFilesContent(".github/workflows/*", true, c, validateGitHubActionWorkflow)
return createResultForIsGitHubActionsWorkflowPinned(r, err)
return createReturnForIsGitHubActionsWorkflowPinned(r, c.Dlogger, err)
}
// Create the result.
func createResultForIsGitHubActionsWorkflowPinned(r bool, err error) checker.CheckResult {
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
}
if r {
return checker.CreateMaxScoreResult(CheckFrozenDeps, "GitHub actions are pinned")
}
return checker.CreateMinScoreResult(CheckFrozenDeps, "GitHub actions are not pinned")
func createReturnForIsGitHubActionsWorkflowPinned(r bool, dl checker.DetailLogger, err error) (int, error) {
return createReturnValues(r,
"GitHub actions are pinned",
dl, err)
}
func testIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker.DetailLogger) checker.CheckResult {
func testIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker.DetailLogger) (int, error) {
r, err := validateGitHubActionWorkflow(pathfn, content, dl)
return createResultForIsGitHubActionsWorkflowPinned(r, err)
return createReturnForIsGitHubActionsWorkflowPinned(r, dl, err)
}
// Check file content.
@ -414,16 +448,17 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.Deta
}
// Check presence of lock files thru validatePackageManagerFile().
func isPackageManagerLockFilePresent(c *checker.CheckRequest) checker.CheckResult {
func isPackageManagerLockFilePresent(c *checker.CheckRequest) (int, error) {
r, err := CheckIfFileExists(CheckFrozenDeps, c, validatePackageManagerFile)
if err != nil {
return checker.CreateRuntimeErrorResult(CheckFrozenDeps, err)
return checker.InconclusiveResultScore, err
}
if !r {
return checker.CreateInconclusiveResult(CheckFrozenDeps, "no lock files detected for a package manager")
c.Dlogger.Warn("no lock files detected for a package manager")
return checker.InconclusiveResultScore, nil
}
return checker.CreateMaxScoreResult(CheckFrozenDeps, "lock file detected for a package manager")
return checker.MaxResultScore, nil
}
// validatePackageManagerFile will validate the if frozen dependecies file name exists.

View File

@ -50,7 +50,7 @@ func TestGithubWorkflowPinning(t *testing.T) {
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
},
},
@ -81,8 +81,8 @@ func TestGithubWorkflowPinning(t *testing.T) {
}
}
dl := scut.TestDetailLogger{}
r := testIsGitHubActionsWorkflowPinned(tt.filename, content, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.expected, &r, &dl)
s, e := testIsGitHubActionsWorkflowPinned(tt.filename, content, &dl)
scut.ValidateTestValues(t, tt.name, &tt.expected, s, e, &dl)
})
}
}
@ -98,10 +98,10 @@ func TestDockerfilePinning(t *testing.T) {
name: "Invalid dockerfile",
filename: "./testdata/Dockerfile-invalid",
expected: scut.TestReturn{
Errors: []error{sce.ErrScorecardInternal},
Score: checker.InconclusiveResultScore,
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
},
},
@ -112,7 +112,7 @@ func TestDockerfilePinning(t *testing.T) {
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
},
},
@ -123,7 +123,7 @@ func TestDockerfilePinning(t *testing.T) {
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
},
},
@ -165,8 +165,8 @@ func TestDockerfilePinning(t *testing.T) {
}
}
dl := scut.TestDetailLogger{}
r := testValidateDockerfileIsPinned(tt.filename, content, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.expected, &r, &dl)
s, e := testValidateDockerfileIsPinned(tt.filename, content, &dl)
scut.ValidateTestValues(t, tt.name, &tt.expected, s, e, &dl)
})
}
}
@ -207,7 +207,7 @@ func TestDockerfileScriptDownload(t *testing.T) {
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfInfo: 1,
NumberOfDebug: 0,
},
},
@ -277,6 +277,17 @@ func TestDockerfileScriptDownload(t *testing.T) {
NumberOfDebug: 0,
},
},
{
name: "download with some python",
filename: "testdata/Dockerfile-some-python",
expected: scut.TestReturn{
Errors: nil,
Score: checker.MinResultScore,
NumberOfWarn: 1,
NumberOfInfo: 0,
NumberOfDebug: 0,
},
},
}
for _, tt := range tests {
tt := tt // Re-initializing variable so it is not changed while executing the closure below
@ -293,8 +304,8 @@ func TestDockerfileScriptDownload(t *testing.T) {
}
}
dl := scut.TestDetailLogger{}
r := testValidateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.expected, &r, &dl)
s, e := testValidateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestValues(t, tt.name, &tt.expected, s, e, &dl)
})
}
}
@ -366,8 +377,8 @@ func TestShellScriptDownload(t *testing.T) {
}
}
dl := scut.TestDetailLogger{}
r := testValidateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.expected, &r, &dl)
s, e := testValidateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestValues(t, tt.name, &tt.expected, s, e, &dl)
})
}
}
@ -428,8 +439,8 @@ func TestGitHubWorflowRunDownload(t *testing.T) {
}
}
dl := scut.TestDetailLogger{}
r := testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.expected, &r, &dl)
s, e := testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(tt.filename, content, &dl)
scut.ValidateTestValues(t, tt.name, &tt.expected, s, e, &dl)
})
}
}

View File

@ -17,6 +17,7 @@ package checks
import (
"bufio"
"bytes"
"errors"
"fmt"
"net/url"
"path"
@ -30,14 +31,16 @@ import (
sce "github.com/ossf/scorecard/v2/errors"
)
// List of interpreters.
var pythonInterpreters = []string{"python", "python3", "python2.7"}
var interpreters = append([]string{
"sh", "bash", "dash", "ksh", "mksh", "python",
"perl", "ruby", "php", "node", "nodejs", "java",
"exec", "su",
}, pythonInterpreters...)
var (
shellNames = []string{
"sh", "bash", "dash", "ksh", "mksh",
}
pythonInterpreters = []string{"python", "python3", "python2.7"}
shellInterpreters = append([]string{"exec", "su"}, shellNames...)
otherInterpreters = []string{"perl", "ruby", "php", "node", "nodejs", "java"}
interpreters = append(otherInterpreters,
append(shellInterpreters, append(shellNames, pythonInterpreters...)...)...)
)
// Note: aws is handled separately because it uses different
// cli options.
@ -45,10 +48,6 @@ var downloadUtils = []string{
"curl", "wget", "gsutil",
}
var shellNames = []string{
"sh", "bash", "dash", "ksh", "mksh",
}
func isBinaryName(expected, name string) bool {
return strings.EqualFold(path.Base(name), expected)
}
@ -200,17 +199,34 @@ func isInterpreter(cmd []string) bool {
return false
}
func isInterpreterWithCommand(cmd []string) bool {
func isShellInterpreterOrCommand(cmd []string) bool {
if len(cmd) == 0 {
return false
}
for _, b := range interpreters {
if isCommand(cmd, b) {
return true
if isPythonCommand(cmd) {
return false
}
for _, b := range otherInterpreters {
if isBinaryName(b, cmd[0]) {
return false
}
}
return false
return true
}
func extractInterpreterAndCommand(cmd []string) (string, bool) {
if len(cmd) == 0 {
return "", false
}
for _, b := range interpreters {
if isCommand(cmd, b) {
return b, true
}
}
return "", false
}
func isInterpreterWithFile(cmd []string, fn string) bool {
@ -620,27 +636,28 @@ func extractInterpreterCommandFromArgs(args []*syntax.Word) (string, bool) {
return "", false
}
func extractInterpreterCommandFromNode(node syntax.Node) (string, bool) {
func extractInterpreterAndCommandFromNode(node syntax.Node) (interpreter, command string, yes bool) {
ce, ok := node.(*syntax.CallExpr)
if !ok {
return "", false
return "", "", false
}
c, ok := extractCommand(ce)
if !ok {
return "", false
return "", "", false
}
if !isInterpreterWithCommand(c) {
return "", false
i, ok := extractInterpreterAndCommand(c)
if !ok {
return "", "", false
}
cs, ok := extractInterpreterCommandFromArgs(ce.Args)
if !ok {
return "", false
return "", "", false
}
return cs, true
return i, cs, true
}
func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) {
@ -660,9 +677,11 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string]
in := strings.NewReader(string(content))
f, err := syntax.NewParser().Parse(in, "")
if err != nil {
// Note: this is caught by internal caller and only printed
// to avoid failing on shell scripts that our parser does not understand.
// Example: https://github.com/openssl/openssl/blob/master/util/shlib_wrap.sh.in
//nolint
return false, sce.Create(sce.ErrScorecardInternal,
fmt.Sprintf("%v: %v", errInternalInvalidShellCode, err))
return false, sce.CreateInternal(errInternalInvalidShellCode, err.Error())
}
printer := syntax.NewPrinter()
@ -675,10 +694,13 @@ func validateShellFileAndRecord(pathfn string, content []byte, files map[string]
return false
}
// sh -c "CMD".
c, ok := extractInterpreterCommandFromNode(node)
// interpreter -c "CMD".
i, c, ok := extractInterpreterAndCommandFromNode(node)
// TODO: support other interpreters.
// Example: https://github.com/apache/airflow/blob/main/scripts/ci/kubernetes/ci_run_kubernetes_tests.sh#L75
// HOST_PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')``
// nolinter
if ok {
if ok && isShellInterpreterOrCommand([]string{i}) {
ok, e := validateShellFileAndRecord(pathfn, []byte(c), files, dl)
validated = ok
if e != nil {
@ -752,28 +774,33 @@ func isShellScriptFile(pathfn string, content []byte) bool {
// Look at file content.
r := strings.NewReader(string(content))
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
// TODO: support perl scripts with embedded shell scripts:
// https://github.com/openssl/openssl/blob/master/test/recipes/15-test_dsaparam.t.
// #!/bin/XXX, #!XXX, #!/usr/bin/env XXX, #!env XXX
if !strings.HasPrefix(line, "#!") {
continue
// Only look at first line.
if !scanner.Scan() {
return false
}
line := scanner.Text()
// #!/bin/XXX, #!XXX, #!/usr/bin/env XXX, #!env XXX
if !strings.HasPrefix(line, "#!") {
return false
}
line = line[2:]
for _, name := range shellNames {
parts := strings.Split(line, " ")
// #!/bin/bash, #!bash -e
if len(parts) >= 1 && isBinaryName(name, parts[0]) {
return true
}
line = line[2:]
for _, name := range shellNames {
parts := strings.Split(line, " ")
// #!/bin/bash, #!bash -e
if len(parts) >= 1 && isBinaryName(name, parts[0]) {
return true
}
// #!/bin/env bash
if len(parts) >= 2 &&
isBinaryName("env", parts[0]) &&
isBinaryName(name, parts[1]) {
return true
}
// #!/bin/env bash
if len(parts) >= 2 &&
isBinaryName("env", parts[0]) &&
isBinaryName(name, parts[1]) {
return true
}
}
@ -782,5 +809,14 @@ func isShellScriptFile(pathfn string, content []byte) bool {
func validateShellFile(pathfn string, content []byte, dl checker.DetailLogger) (bool, error) {
files := make(map[string]bool)
return validateShellFileAndRecord(pathfn, content, files, dl)
r, err := validateShellFileAndRecord(pathfn, content, files, dl)
if err != nil {
if errors.Is(err, errInternalInvalidShellCode) {
// Discard and print this particular error for now.
dl.Debug(err.Error())
} else {
return r, err
}
}
return r, nil
}

View File

@ -16,12 +16,17 @@
FROM python:3.7@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
RUN bash <(wget -qO- http://website.com/my-script.sh)
RUN bash <(curl -s https://codecov.io/bash)
RUN ["bash", "<(curl -s https://codecov.io/bash)"]
RUN bash <(curl -s https://codecov.io/bash1)
RUN ["bash", "<(curl -s https://codecov.io/bash2)"]
RUN sudo su -c "bash <(wget -qO- http://website.com/my-script.sh)" root
RUN sudo su -c "bash <(curl -s https://codecov.io/bash)" root
RUN ["su", "-c", "\"bash <(curl -s https://codecov.io/bash)\""]
RUN sudo su -c "bash <(wget -qO- http://website.com/my-script2.sh)" root
RUN sudo su -c "bash <(curl -s https://codecov.io/bash3)" root
RUN ["su", "-c", "\"bash <(curl -s https://codecov.io/bash4)\""]
FROM scratch
FROM python@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
FROM python@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
[{1 insecure (unpinned) download detected in testdata/Dockerfile-proc-subs: 'bash <(wget -qO- http://website.com/my-script.sh)'}
{1 insecure (unpinned) download detected in testdata/Dockerfile-proc-subs: 'bash <(curl -s https://codecov.io/bash)'}
{1 insecure (unpinned) download detected in testdata/Dockerfile-proc-subs: 'bash <(curl -s https://codecov.io/bash)'}]
FAIL

31
checks/testdata/Dockerfile-some-python vendored Normal file
View File

@ -0,0 +1,31 @@
# 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.
# Taken from https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/dockerfiles/dockerfiles/ppc64le/cpu-ppc64le-jupyter.Dockerfile
ARG CACHE_STOP=1
RUN if [ ${TF_PACKAGE} = tensorflow-gpu ]; then \
BASE=https://powerci.osuosl.org/job/TensorFlow_PPC64LE_GPU_Release_Build/lastSuccessfulBuild/; \
elif [ ${TF_PACKAGE} = tf-nightly-gpu ]; then \
BASE=https://powerci.osuosl.org/job/TensorFlow_PPC64LE_GPU_Nightly_Artifact/lastSuccessfulBuild/; \
elif [ ${TF_PACKAGE} = tensorflow ]; then \
BASE=https://powerci.osuosl.org/job/TensorFlow_PPC64LE_CPU_Release_Build/lastSuccessfulBuild/; \
elif [ ${TF_PACKAGE} = tf-nightly ]; then \
BASE=https://powerci.osuosl.org/job/TensorFlow_PPC64LE_CPU_Nightly_Artifact/lastSuccessfulBuild/; \
fi; \
MAJOR=`python3 -c 'import sys; print(sys.version_info[0])'`; \
MINOR=`python3 -c 'import sys; print(sys.version_info[1])'`; \
PACKAGE=$(wget -qO- ${BASE}"api/xml?xpath=//fileName&wrapper=artifacts" | grep -o "[^<>]*cp${MAJOR}${MINOR}[^<>]*.whl"); \
wget ${BASE}"artifact/tensorflow_pkg/"${PACKAGE}; \
python3 -m pip install --no-cache-dir ${PACKAGE}

View File

@ -48,10 +48,10 @@ var _ = Describe("E2E TEST:FrozenDeps", func() {
}
expected := scut.TestReturn{
Errors: nil,
Score: checker.InconclusiveResultScore,
NumberOfWarn: 222,
Score: checker.MinResultScore,
NumberOfWarn: 374,
NumberOfInfo: 0,
NumberOfDebug: 0,
NumberOfDebug: 4,
}
result := checks.FrozenDeps(&req)
// UPGRADEv2: to remove.
@ -81,7 +81,7 @@ var _ = Describe("E2E TEST:FrozenDeps", func() {
Errors: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 6,
NumberOfDebug: 0,
}
result := checks.FrozenDeps(&req)

View File

@ -36,3 +36,9 @@ func Create(e error, msg string) error {
// We still need to use %w to prevent callers from using e == ErrInvalidDockerFile.
return fmt.Errorf("%w", e)
}
// Create an internal error, not using
// any of the errors listed above.
func CreateInternal(e error, msg string) error {
return Create(e, msg)
}

View File

@ -38,6 +38,7 @@ func validateDetailTypes(messages []checker.CheckDetail, nw, ni, nd int) bool {
enw++
}
}
return enw == nw &&
eni == ni &&
end == nd
@ -71,26 +72,35 @@ func (l *TestDetailLogger) Debug(desc string, args ...interface{}) {
}
//nolint
func ValidateTestReturn(t *testing.T, name string, te *TestReturn,
tr *checker.CheckResult, dl *TestDetailLogger) bool {
func ValidateTestValues(t *testing.T, name string, te *TestReturn,
score int, err error, dl *TestDetailLogger) bool {
for _, we := range te.Errors {
if !errors.Is(tr.Error2, we) {
if !errors.Is(err, we) {
if t != nil {
t.Errorf("%v: invalid error returned: %v is not of type %v",
name, tr.Error, we)
name, err, we)
}
fmt.Printf("%v: invalid error returned: %v is not of type %v",
name, err, we)
return false
}
}
// UPGRADEv2: update name.
if tr.Score != te.Score ||
if score != te.Score ||
!validateDetailTypes(dl.messages, te.NumberOfWarn,
te.NumberOfInfo, te.NumberOfDebug) {
if t != nil {
t.Errorf("%v: Got (score=%v) expected (%v)\n%v",
name, tr.Score, te.Score, dl.messages)
name, score, te.Score, dl.messages)
}
return false
}
return true
}
//nolint
func ValidateTestReturn(t *testing.T, name string, te *TestReturn,
tr *checker.CheckResult, dl *TestDetailLogger) bool {
return ValidateTestValues(t, name, te, tr.Score, tr.Error2, dl)
}