diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8d658545320..91d1b9d7948 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -879,5 +879,5 @@ jobs: run: | go env -w GOFLAGS=-mod=mod go install github.com/google/addlicense@2fe3ee94479d08be985a84861de4e6b06a1c7208 - addlicense -ignore "**/script-empty.sh" -ignore "pkg/testdata/**" -ignore "checks/testdata/**" -l apache -c 'Security Scorecard Authors' -v * + addlicense -ignore "**/script-empty.sh" -ignore "testdata/**" -ignore "**/testdata/**" -l apache -c 'Security Scorecard Authors' -v * git diff --exit-code diff --git a/checker/raw_result.go b/checker/raw_result.go index dbf69563c07..2e930fb4a8b 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -31,6 +31,7 @@ type RawResults struct { DependencyUpdateToolResults DependencyUpdateToolData BranchProtectionResults BranchProtectionsData CodeReviewResults CodeReviewData + PinningDependenciesResults PinningDependenciesData WebhookResults WebhooksData ContributorsResults ContributorsData MaintainedResults MaintainedData @@ -64,6 +65,42 @@ type Package struct { Runs []Run } +// DependencyUseType reprensets a type of dependency use. +type DependencyUseType string + +const ( + // DependencyUseTypeGHAction is an action. + DependencyUseTypeGHAction DependencyUseType = "GitHubAction" + // DependencyUseTypeDockerfileContainerImage a container image used via FROM. + DependencyUseTypeDockerfileContainerImage DependencyUseType = "containerImage" + // DependencyUseTypeDownloadThenRun is a download followed by a run. + DependencyUseTypeDownloadThenRun DependencyUseType = "downloadThenRun" + // DependencyUseTypeGoCommand is a go command. + DependencyUseTypeGoCommand DependencyUseType = "goCommand" + // DependencyUseTypeChocoCommand is a choco command. + DependencyUseTypeChocoCommand DependencyUseType = "chocoCommand" + // DependencyUseTypeNpmCommand is an npm command. + DependencyUseTypeNpmCommand DependencyUseType = "npmCommand" + // DependencyUseTypePipCommand is a pipp command. + DependencyUseTypePipCommand DependencyUseType = "pipCommand" +) + +// PinningDependenciesData represents pinned dependency data. +type PinningDependenciesData struct { + Dependencies []Dependency +} + +// Dependency represents a dependency. +type Dependency struct { + // TODO: unique dependency name. + // TODO: Job *WorkflowJob + Name *string + PinnedAt *string + Location *File + Msg *string // Only for debug messages. + Type DependencyUseType +} + // MaintainedData contains the raw results // for the Maintained check. type MaintainedData struct { @@ -165,10 +202,11 @@ type ArchivedStatus struct { // File represents a file. type File struct { - Path string - Snippet string // Snippet of code - Offset uint // Offset in the file of Path (line for source/text files). - Type FileType // Type of file. + Path string + Snippet string // Snippet of code + Offset uint // Offset in the file of Path (line for source/text files). + EndOffset uint // End of offset in the file, e.g. if the command spans multiple lines. + Type FileType // Type of file. // TODO: add hash. } diff --git a/checks/errors.go b/checks/errors.go index e8cd0d624d0..c99c61f1d2f 100644 --- a/checks/errors.go +++ b/checks/errors.go @@ -19,7 +19,6 @@ import ( ) var ( - errInternalInvalidDockerFile = errors.New("invalid Dockerfile") errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow") errInternalNameCannotBeEmpty = errors.New("name cannot be empty") errInternalCheckFuncCannotBeNil = errors.New("checkFunc cannot be nil") diff --git a/checks/evaluation/pinned_dependencies.go b/checks/evaluation/pinned_dependencies.go new file mode 100644 index 00000000000..4701b7e4417 --- /dev/null +++ b/checks/evaluation/pinned_dependencies.go @@ -0,0 +1,292 @@ +// 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 evaluation + +import ( + "errors" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/checks/fileparser" + sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/remediation" +) + +var errInvalidValue = errors.New("invalid value") + +type pinnedResult int + +const ( + pinnedUndefined pinnedResult = iota + pinned + notPinned +) + +// Structure to host information about pinned github +// or third party dependencies. +type worklowPinningResult struct { + thirdParties pinnedResult + gitHubOwned pinnedResult +} + +// PinningDependencies applies the score policy for the Pinned-Dependencies check. +func PinningDependencies(name string, dl checker.DetailLogger, + r *checker.PinningDependenciesData, +) checker.CheckResult { + if r == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") + return checker.CreateRuntimeErrorResult(name, e) + } + + var wp worklowPinningResult + pr := make(map[checker.DependencyUseType]pinnedResult) + + for i := range r.Dependencies { + rr := r.Dependencies[i] + if rr.Location == nil { + if rr.Msg == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "empty File field") + return checker.CreateRuntimeErrorResult(name, e) + } + dl.Debug(&checker.LogMessage{ + Text: *rr.Msg, + }) + continue + } + + if rr.Msg != nil { + dl.Debug(&checker.LogMessage{ + Path: rr.Location.Path, + Type: rr.Location.Type, + Offset: rr.Location.Offset, + EndOffset: rr.Location.EndOffset, + Text: *rr.Msg, + Snippet: rr.Location.Snippet, + }) + } else { + dl.Warn(&checker.LogMessage{ + Path: rr.Location.Path, + Type: rr.Location.Type, + Offset: rr.Location.Offset, + EndOffset: rr.Location.EndOffset, + Text: generateText(&rr), + Snippet: rr.Location.Snippet, + Remediation: generateRemediation(&rr), + }) + + // Update the pinning status. + updatePinningResults(&rr, &wp, pr) + } + } + + // Generate scores and Info results. + // GitHub actions. + actionScore, err := createReturnForIsGitHubActionsWorkflowPinned(wp, dl) + if err != nil { + return checker.CreateRuntimeErrorResult(name, err) + } + + // Docker files. + dockerFromScore, err := createReturnForIsDockerfilePinned(pr, dl) + if err != nil { + return checker.CreateRuntimeErrorResult(name, err) + } + + // Docker downloads. + dockerDownloadScore, err := createReturnForIsDockerfileFreeOfInsecureDownloads(pr, dl) + if err != nil { + return checker.CreateRuntimeErrorResult(name, err) + } + + // Script downloads. + scriptScore, err := createReturnForIsShellScriptFreeOfInsecureDownloads(pr, dl) + if err != nil { + return checker.CreateRuntimeErrorResult(name, err) + } + + // Scores may be inconclusive. + actionScore = maxScore(0, actionScore) + dockerFromScore = maxScore(0, dockerFromScore) + dockerDownloadScore = maxScore(0, dockerDownloadScore) + scriptScore = maxScore(0, scriptScore) + + score := checker.AggregateScores(actionScore, dockerFromScore, + dockerDownloadScore, scriptScore) + + if score == checker.MaxResultScore { + return checker.CreateMaxScoreResult(name, "all dependencies are pinned") + } + + return checker.CreateProportionalScoreResult(name, + "dependency not pinned by hash detected", score, checker.MaxResultScore) +} + +func generateRemediation(rr *checker.Dependency) *checker.Remediation { + if rr.Type == checker.DependencyUseTypeGHAction { + return remediation.CreateWorkflowPinningRemediation(rr.Location.Path) + } + return nil +} + +func updatePinningResults(rr *checker.Dependency, + wp *worklowPinningResult, pr map[checker.DependencyUseType]pinnedResult, +) { + if rr.Type == checker.DependencyUseTypeGHAction { + // Note: `Snippet` contains `action/name@xxx`, so we cna use it to infer + // if it's a GitHub-owned action or not. + gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet) + addWorkflowPinnedResult(wp, false, gitHubOwned) + return + } + + // Update other result types. + var p pinnedResult + addPinnedResult(&p, false) + pr[rr.Type] = p +} + +func generateText(rr *checker.Dependency) string { + if rr.Type == checker.DependencyUseTypeGHAction { + // Check if we are dealing with a GitHub action or a third-party one. + gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet) + owner := generateOwnerToDisplay(gitHubOwned) + return fmt.Sprintf("%s %s not pinned by hash", owner, rr.Type) + } + + return fmt.Sprintf("%s not pinned by hash", rr.Type) +} + +func generateOwnerToDisplay(gitHubOwned bool) string { + if gitHubOwned { + return "GitHub-owned" + } + return "third-party" +} + +// TODO(laurent): need to support GCB pinning. +//nolint +func maxScore(s1, s2 int) int { + if s1 > s2 { + return s1 + } + return s2 +} + +// For the 'to' param, true means the file is pinning dependencies (or there are no dependencies), +// false means there are unpinned dependencies. +func addPinnedResult(r *pinnedResult, to bool) { + // If the result is `notPinned`, we keep it. + // In other cases, we always update the result. + if *r == notPinned { + return + } + + switch to { + case true: + *r = pinned + case false: + *r = notPinned + } +} + +func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) { + if isGitHub { + addPinnedResult(&w.gitHubOwned, to) + } else { + addPinnedResult(&w.thirdParties, to) + } +} + +// Create the result for scripts. +func createReturnForIsShellScriptFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult, + dl checker.DetailLogger, +) (int, error) { + return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun, + "no insecure (not pinned by hash) dependency downloads found in shell scripts", + dl) +} + +// Create the result for docker containers. +func createReturnForIsDockerfilePinned(pr map[checker.DependencyUseType]pinnedResult, + dl checker.DetailLogger, +) (int, error) { + return createReturnValues(pr, checker.DependencyUseTypeDockerfileContainerImage, + "Dockerfile dependencies are pinned", + dl) +} + +// Create the result for docker commands. +func createReturnForIsDockerfileFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult, + dl checker.DetailLogger, +) (int, error) { + return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun, + "no insecure (not pinned by hash) dependency downloads found in Dockerfiles", + dl) +} + +func createReturnValues(pr map[checker.DependencyUseType]pinnedResult, + t checker.DependencyUseType, infoMsg string, + dl checker.DetailLogger, +) (int, error) { + // Note: we don't check if the entry exists, + // as it will have the default value which is handled in the switch statement. + //nolint + r, _ := pr[t] + switch r { + default: + return checker.InconclusiveResultScore, fmt.Errorf("%w: %v", errInvalidValue, r) + case pinned, pinnedUndefined: + dl.Info(&checker.LogMessage{ + Text: infoMsg, + }) + return checker.MaxResultScore, nil + case notPinned: + // No logging needed as it's done by the checks. + return checker.MinResultScore, nil + } +} + +// Create the result. +func createReturnForIsGitHubActionsWorkflowPinned(wp worklowPinningResult, dl checker.DetailLogger) (int, error) { + return createReturnValuesForGitHubActionsWorkflowPinned(wp, + fmt.Sprintf("%ss are pinned", checker.DependencyUseTypeGHAction), + dl) +} + +func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string, + dl checker.DetailLogger, +) (int, error) { + score := checker.MinResultScore + + if r.gitHubOwned != notPinned { + score += 2 + dl.Info(&checker.LogMessage{ + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + Text: fmt.Sprintf("%s %s", "GitHub-owned", infoMsg), + }) + } + + if r.thirdParties != notPinned { + score += 8 + dl.Info(&checker.LogMessage{ + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + Text: fmt.Sprintf("%s %s", "Third-party", infoMsg), + }) + } + + return score, nil +} diff --git a/checks/evaluation/pinned_dependencies_test.go b/checks/evaluation/pinned_dependencies_test.go new file mode 100644 index 00000000000..387821fcc5c --- /dev/null +++ b/checks/evaluation/pinned_dependencies_test.go @@ -0,0 +1,312 @@ +// Copyright 2020 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package evaluation + +import ( + "testing" + + "github.com/ossf/scorecard/v4/checker" + scut "github.com/ossf/scorecard/v4/utests" +) + +func Test_createReturnValuesForGitHubActionsWorkflowPinned(t *testing.T) { + t.Parallel() + //nolint + type args struct { + r worklowPinningResult + infoMsg string + dl checker.DetailLogger + } + //nolint + tests := []struct { + name string + args args + want int + }{ + { + name: "both actions workflow pinned", + args: args{ + r: worklowPinningResult{ + thirdParties: 1, + gitHubOwned: 1, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 10, + }, + { + name: "github actions workflow pinned", + args: args{ + r: worklowPinningResult{ + thirdParties: 2, + gitHubOwned: 2, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 0, + }, + { + name: "error in github actions workflow pinned", + args: args{ + r: worklowPinningResult{ + thirdParties: 2, + gitHubOwned: 2, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 0, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := createReturnValuesForGitHubActionsWorkflowPinned(tt.args.r, tt.args.infoMsg, tt.args.dl) + if err != nil { + t.Errorf("error during createReturnValuesForGitHubActionsWorkflowPinned: %v", err) + } + if got != tt.want { + t.Errorf("createReturnValuesForGitHubActionsWorkflowPinned() = %v, want %v", got, tt.want) + } + }) + } +} + +func asPointer(s string) *string { + return &s +} + +func Test_PinningDependencies(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dependencies []checker.Dependency + expected scut.TestReturn + }{ + { + name: "download then run pinned debug", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Msg: asPointer("some message"), + Type: checker.DependencyUseTypeDownloadThenRun, + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 5, + NumberOfDebug: 1, + }, + }, + { + name: "download then run pinned debug and warn", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Msg: asPointer("some message"), + Type: checker.DependencyUseTypeDownloadThenRun, + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeDownloadThenRun, + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 5, + NumberOfWarn: 1, + NumberOfInfo: 3, + NumberOfDebug: 1, + }, + }, + { + name: "various wanrings", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeDownloadThenRun, + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeDockerfileContainerImage, + }, + { + Location: &checker.File{}, + Msg: asPointer("debug message"), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 2, + NumberOfWarn: 3, + NumberOfInfo: 2, + NumberOfDebug: 1, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + dl := scut.TestDetailLogger{} + actual := PinningDependencies("checkname", &dl, + &checker.PinningDependenciesData{ + Dependencies: tt.dependencies, + }) + + if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { + t.Fail() + } + }) + } +} + +func Test_createReturnValues(t *testing.T) { + t.Parallel() + + type args struct { + pr map[checker.DependencyUseType]pinnedResult + dl *scut.TestDetailLogger + t checker.DependencyUseType + } + + tests := []struct { + name string + args args + want int + }{ + { + name: "returns 10 if no error and no pinnedResult", + args: args{ + t: checker.DependencyUseTypeDownloadThenRun, + dl: &scut.TestDetailLogger{}, + }, + want: 10, + }, + { + name: "returns 10 if pinned undefined", + args: args{ + t: checker.DependencyUseTypeDownloadThenRun, + pr: map[checker.DependencyUseType]pinnedResult{ + checker.DependencyUseTypeDownloadThenRun: pinnedUndefined, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 10, + }, + { + name: "returns 10 if pinned", + args: args{ + t: checker.DependencyUseTypeDownloadThenRun, + pr: map[checker.DependencyUseType]pinnedResult{ + checker.DependencyUseTypeDownloadThenRun: pinned, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 10, + }, + { + name: "returns 0 if unpinned", + args: args{ + t: checker.DependencyUseTypeDownloadThenRun, + pr: map[checker.DependencyUseType]pinnedResult{ + checker.DependencyUseTypeDownloadThenRun: notPinned, + }, + dl: &scut.TestDetailLogger{}, + }, + want: 0, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := createReturnValues(tt.args.pr, tt.args.t, "some message", tt.args.dl) + if err != nil { + t.Errorf("error during createReturnValues: %v", err) + } + if got != tt.want { + t.Errorf("createReturnValues() = %v, want %v", got, tt.want) + } + + if tt.want < 10 { + return + } + + isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { + return logMessage.Text == "some message" && logType == checker.DetailInfo + } + if !scut.ValidateLogMessage(isExpectedLog, tt.args.dl) { + t.Errorf("test failed: log message not present: %+v", "some message") + } + }) + } +} + +func Test_maxScore(t *testing.T) { + t.Parallel() + type args struct { + s1 int + s2 int + } + tests := []struct { + name string + args args + want int + }{ + { + name: "returns s1 if s1 is greater than s2", + args: args{ + s1: 10, + s2: 5, + }, + want: 10, + }, + { + name: "returns s2 if s2 is greater than s1", + args: args{ + s1: 5, + s2: 10, + }, + want: 10, + }, + { + name: "returns s1 if s1 is equal to s2", + args: args{ + s1: 10, + s2: 10, + }, + want: 10, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := maxScore(tt.args.s1, tt.args.s2); got != tt.want { + t.Errorf("maxScore() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/checks/pinned_dependencies.go b/checks/pinned_dependencies.go index 71348054b43..915d6465351 100644 --- a/checks/pinned_dependencies.go +++ b/checks/pinned_dependencies.go @@ -15,716 +15,47 @@ package checks import ( - "errors" "fmt" - "regexp" - "strings" - - "github.com/moby/buildkit/frontend/dockerfile/parser" - "github.com/rhysd/actionlint" "github.com/ossf/scorecard/v4/checker" - "github.com/ossf/scorecard/v4/checks/fileparser" + "github.com/ossf/scorecard/v4/checks/evaluation" + "github.com/ossf/scorecard/v4/checks/raw" sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/remediation" ) // CheckPinnedDependencies is the registered name for FrozenDeps. const CheckPinnedDependencies = "Pinned-Dependencies" -// Structure to host information about pinned github -// or third party dependencies. -type worklowPinningResult struct { - thirdParties pinnedResult - gitHubOwned pinnedResult -} - //nolint:gochecknoinits func init() { supportedRequestTypes := []checker.RequestType{ checker.FileBased, checker.CommitBased, } - if err := registerCheck(CheckPinnedDependencies, PinnedDependencies, supportedRequestTypes); err != nil { + if err := registerCheck(CheckPinnedDependencies, PinningDependencies, supportedRequestTypes); err != nil { // This should never happen. panic(err) } } -// PinnedDependencies will check the repository if it contains frozen dependecies. -func PinnedDependencies(c *checker.CheckRequest) checker.CheckResult { - // Lock file. - /* WARNING: this code is inherently incorrect: - - does not differentiate between libs and main - - only looks at root folder. - => disabling to avoid false positives. - lockScore, lockErr := isPackageManagerLockFilePresent(c) - if lockErr != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, lockErr) - } - */ - - if remErr := remdiationSetup(c); remErr != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, remErr) - } - - // GitHub actions. - actionScore, actionErr := isGitHubActionsWorkflowPinned(c) - if actionErr != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, actionErr) - } - - // Docker files. - dockerFromScore, dockerFromErr := isDockerfilePinned(c) - if dockerFromErr != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, dockerFromErr) - } - - // Docker downloads. - dockerDownloadScore, dockerDownloadErr := isDockerfileFreeOfInsecureDownloads(c) - if dockerDownloadErr != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, dockerDownloadErr) +// PinningDependencies will check the repository for its use of dependencies. +func PinningDependencies(c *checker.CheckRequest) checker.CheckResult { + if err := remediation.Setup(c); err != nil { + e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("remdiationSetup: %v", err)) + return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, e) } - // Script downloads. - scriptScore, scriptError := isShellScriptFreeOfInsecureDownloads(c) - if scriptError != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, scriptError) - } - - // Action script downloads. - actionScriptScore, actionScriptError := isGitHubWorkflowScriptFreeOfInsecureDownloads(c) - if actionScriptError != nil { - return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, actionScriptError) - } - - // Scores may be inconclusive. - actionScore = maxScore(0, actionScore) - dockerFromScore = maxScore(0, dockerFromScore) - dockerDownloadScore = maxScore(0, dockerDownloadScore) - scriptScore = maxScore(0, scriptScore) - actionScriptScore = maxScore(0, actionScriptScore) - score := checker.AggregateScores(actionScore, dockerFromScore, - dockerDownloadScore, scriptScore, actionScriptScore) - - if score == checker.MaxResultScore { - return checker.CreateMaxScoreResult(CheckPinnedDependencies, "all dependencies are pinned") - } - return checker.CreateProportionalScoreResult(CheckPinnedDependencies, - "dependency not pinned by hash detected", score, checker.MaxResultScore) -} - -// TODO(laurent): need to support GCB pinning. -//nolint -func maxScore(s1, s2 int) int { - if s1 > s2 { - return s1 - } - return s2 -} - -type pinnedResult int - -const ( - pinnedUndefined pinnedResult = iota - pinned - notPinned -) - -// For the 'to' param, true means the file is pinning dependencies (or there are no dependencies), -// false means there are unpinned dependencies. -func addPinnedResult(r *pinnedResult, to bool) { - // If the result is `notPinned`, we keep it. - // In other cases, we always update the result. - if *r == notPinned { - return - } - - switch to { - case true: - *r = pinned - case false: - *r = notPinned - } -} - -func dataAsWorkflowResultPointer(data interface{}) *worklowPinningResult { - pdata, ok := data.(*worklowPinningResult) - if !ok { - // panic if it is not correct type - panic("type need to be of worklowPinningResult") - } - return pdata -} - -func dataAsResultPointer(data interface{}) *pinnedResult { - pdata, ok := data.(*pinnedResult) - if !ok { - // This never happens. - panic("invalid type") - } - return pdata -} - -func dataAsDetailLogger(data interface{}) checker.DetailLogger { - pdata, ok := data.(checker.DetailLogger) - if !ok { - // This never happens. - panic("invalid type") - } - return pdata -} - -func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string, - dl checker.DetailLogger, err error, -) (int, error) { + rawData, err := raw.PinningDependencies(c) if err != nil { - return checker.InconclusiveResultScore, err + e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + return checker.CreateRuntimeErrorResult(CheckPinnedDependencies, e) } - score := checker.MinResultScore - - if r.gitHubOwned != notPinned { - score += 2 - dl.Info(&checker.LogMessage{ - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, - Text: fmt.Sprintf("%s %s", "GitHub-owned", infoMsg), - }) + // Set the raw results. + if c.RawResults != nil { + c.RawResults.PinningDependenciesResults = rawData } - if r.thirdParties != notPinned { - score += 8 - dl.Info(&checker.LogMessage{ - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, - Text: fmt.Sprintf("%s %s", "Third-party", infoMsg), - }) - } - - return score, nil -} - -func createReturnValues(r pinnedResult, infoMsg string, dl checker.DetailLogger, err error) (int, error) { - if err != nil { - return checker.InconclusiveResultScore, err - } - - switch r { - default: - panic("invalid value") - case pinned, pinnedUndefined: - dl.Info(&checker.LogMessage{ - Text: infoMsg, - }) - return checker.MaxResultScore, nil - case notPinned: - // No logging needed as it's done by the checks. - return checker.MinResultScore, nil - } -} - -func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) { - var r pinnedResult - err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ - Pattern: "*", - CaseSensitive: false, - }, validateShellScriptIsFreeOfInsecureDownloads, c.Dlogger, &r) - return createReturnForIsShellScriptFreeOfInsecureDownloads(r, c.Dlogger, err) -} - -func createReturnForIsShellScriptFreeOfInsecureDownloads(r pinnedResult, - dl checker.DetailLogger, err error, -) (int, error) { - return createReturnValues(r, - "no insecure (not pinned by hash) dependency downloads found in shell scripts", - dl, err) -} - -var validateShellScriptIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( - pathfn string, - content []byte, - args ...interface{}, -) (bool, error) { - if len(args) != 2 { - return false, fmt.Errorf( - "validateShellScriptIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength) - } - - pdata := dataAsResultPointer(args[1]) - dl := dataAsDetailLogger(args[0]) - - // Validate the file type. - if !isSupportedShellScriptFile(pathfn, content) { - addPinnedResult(pdata, true) - return true, nil - } - r, err := validateShellFile(pathfn, 0, 0 /*unknown*/, content, map[string]bool{}, dl) - if err != nil { - // Ignore parsing errors. - if errors.Is(err, sce.ErrorShellParsing) { - addPinnedResult(pdata, true) - } - - return false, nil - } - - addPinnedResult(pdata, r) - return true, nil -} - -func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) { - var r pinnedResult - err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ - Pattern: "*Dockerfile*", - CaseSensitive: false, - }, validateDockerfileIsFreeOfInsecureDownloads, c.Dlogger, &r) - return createReturnForIsDockerfileFreeOfInsecureDownloads(r, c.Dlogger, err) -} - -// Create the result. -func createReturnForIsDockerfileFreeOfInsecureDownloads(r pinnedResult, - dl checker.DetailLogger, err error, -) (int, error) { - return createReturnValues(r, - "no insecure (not pinned by hash) dependency downloads found in Dockerfiles", - dl, err) -} - -func isDockerfile(pathfn string, content []byte) bool { - if strings.HasSuffix(pathfn, ".go") || - strings.HasSuffix(pathfn, ".c") || - strings.HasSuffix(pathfn, ".cpp") || - strings.HasSuffix(pathfn, ".rs") || - strings.HasSuffix(pathfn, ".js") || - strings.HasSuffix(pathfn, ".py") || - strings.HasSuffix(pathfn, ".pyc") || - strings.HasSuffix(pathfn, ".java") || - isShellScriptFile(pathfn, content) { - return false - } - return true -} - -var validateDockerfileIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( - pathfn string, - content []byte, - args ...interface{}, -) (bool, error) { - if len(args) != 2 { - return false, fmt.Errorf( - "validateDockerfileIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength) - } - pdata := dataAsResultPointer(args[1]) - dl := dataAsDetailLogger(args[0]) - - // Return early if this is not a docker file. - if !isDockerfile(pathfn, content) { - addPinnedResult(pdata, true) - return true, nil - } - - if !fileparser.CheckFileContainsCommands(content, "#") { - addPinnedResult(pdata, true) - return true, nil - } - - contentReader := strings.NewReader(string(content)) - res, err := parser.Parse(contentReader) - if err != nil { - return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("%v: %v", errInternalInvalidDockerFile, err)) - } - - // Walk the Dockerfile's AST. - taintedFiles := make(map[string]bool) - for i := range res.AST.Children { - var bytes []byte - - child := res.AST.Children[i] - cmdType := child.Value - - // Only look for the 'RUN' command. - if cmdType != "RUN" { - continue - } - - var valueList []string - for n := child.Next; n != nil; n = n.Next { - valueList = append(valueList, n.Value) - } - - if len(valueList) == 0 { - return false, sce.WithMessage(sce.ErrScorecardInternal, errInternalInvalidDockerFile.Error()) - } - - // Build a file content. - cmd := strings.Join(valueList, " ") - bytes = append(bytes, cmd...) - r, err := validateShellFile(pathfn, uint(child.StartLine)-1, uint(child.EndLine)-1, - bytes, taintedFiles, dl) - if err != nil { - // Ignore parsing errors. - if errors.Is(err, sce.ErrorShellParsing) { - addPinnedResult(pdata, true) - } - return false, err - } - addPinnedResult(pdata, r) - } - - return true, nil -} - -func isDockerfilePinned(c *checker.CheckRequest) (int, error) { - var r pinnedResult - err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ - Pattern: "*Dockerfile*", - CaseSensitive: false, - }, validateDockerfileIsPinned, c.Dlogger, &r) - return createReturnForIsDockerfilePinned(r, c.Dlogger, err) -} - -// Create the result. -func createReturnForIsDockerfilePinned(r pinnedResult, dl checker.DetailLogger, err error) (int, error) { - return createReturnValues(r, - "Dockerfile dependencies are pinned", - dl, err) -} - -var validateDockerfileIsPinned fileparser.DoWhileTrueOnFileContent = func( - pathfn string, - content []byte, - args ...interface{}, -) (bool, error) { - // Users may use various names, e.g., - // Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template - - if len(args) != 2 { - return false, fmt.Errorf( - "validateDockerfileIsPinned requires exactly 2 arguments: %w", errInvalidArgLength) - } - pdata := dataAsResultPointer(args[1]) - dl := dataAsDetailLogger(args[0]) - // Return early if this is not a dockerfile. - if !isDockerfile(pathfn, content) { - addPinnedResult(pdata, true) - return true, nil - } - - if !fileparser.CheckFileContainsCommands(content, "#") { - addPinnedResult(pdata, true) - return true, nil - } - - if fileparser.IsTemplateFile(pathfn) { - addPinnedResult(pdata, true) - 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)) - // The dependency must be pinned by sha256 hash, e.g., - // FROM something@sha256:${ARG}, - // FROM something:@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 - regex := regexp.MustCompile(`.*@sha256:([a-f\d]{64}|\${.*})`) - - ret := true - pinnedAsNames := make(map[string]bool) - res, err := parser.Parse(contentReader) - if err != nil { - return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("%v: %v", errInternalInvalidDockerFile, err)) - } - - for _, child := range res.AST.Children { - cmdType := child.Value - if cmdType != "FROM" { - continue - } - - var valueList []string - for n := child.Next; n != nil; n = n.Next { - valueList = append(valueList, n.Value) - } - - switch { - // scratch is no-op. - case len(valueList) > 0 && strings.EqualFold(valueList[0], "scratch"): - continue - - // FROM name AS newname. - case len(valueList) == 3 && strings.EqualFold(valueList[1], "as"): - name := valueList[0] - asName := valueList[2] - // Check if the name is pinned. - // (1): name = <>@sha245:hash - // (2): name = XXX where XXX was pinned - pinned := pinnedAsNames[name] - if pinned || regex.MatchString(name) { - // Record the asName. - pinnedAsNames[asName] = true - continue - } - - // Not pinned. - ret = false - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: uint(child.StartLine), - EndOffset: uint(child.EndLine), - Text: "docker image not pinned by hash", - Snippet: child.Original, - }) - - // FROM name. - case len(valueList) == 1: - name := valueList[0] - pinned := pinnedAsNames[name] - if !pinned && !regex.MatchString(name) { - ret = false - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: uint(child.StartLine), - EndOffset: uint(child.EndLine), - Text: "docker image not pinned by hash", - Snippet: child.Original, - }) - } - - default: - // That should not happen. - return false, sce.WithMessage(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. - - addPinnedResult(pdata, ret) - return true, nil -} - -func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) { - var r pinnedResult - err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ - Pattern: ".github/workflows/*", - CaseSensitive: false, - }, validateGitHubWorkflowIsFreeOfInsecureDownloads, c.Dlogger, &r) - return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, c.Dlogger, err) -} - -// Create the result. -func createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r pinnedResult, - dl checker.DetailLogger, err error, -) (int, error) { - return createReturnValues(r, - "no insecure (not pinned by hash) dependency downloads found in GitHub workflows", - dl, err) -} - -// validateGitHubWorkflowIsFreeOfInsecureDownloads checks if the workflow file downloads dependencies that are unpinned. -// Returns true if the check should continue executing after this file. -var validateGitHubWorkflowIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( - pathfn string, - content []byte, - args ...interface{}, -) (bool, error) { - if !fileparser.IsWorkflowFile(pathfn) { - return true, nil - } - - if len(args) != 2 { - return false, fmt.Errorf( - "validateGitHubWorkflowIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength) - } - pdata := dataAsResultPointer(args[1]) - dl := dataAsDetailLogger(args[0]) - - if !fileparser.CheckFileContainsCommands(content, "#") { - addPinnedResult(pdata, true) - return true, nil - } - - workflow, errs := actionlint.Parse(content) - if len(errs) > 0 && workflow == nil { - // actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards. - // Often we don't care about these errors. - return false, fileparser.FormatActionlintError(errs) - } - - githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) - for jobName, job := range workflow.Jobs { - jobName := jobName - job := job - if len(fileparser.GetJobName(job)) > 0 { - jobName = fileparser.GetJobName(job) - } - taintedFiles := make(map[string]bool) - for _, step := range job.Steps { - step := step - if !fileparser.IsStepExecKind(step, actionlint.ExecKindRun) { - continue - } - - execRun, ok := step.Exec.(*actionlint.ExecRun) - if !ok { - stepName := fileparser.GetStepName(step) - return false, sce.WithMessage(sce.ErrScorecardInternal, - fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName)) - } - - if execRun == nil || execRun.Run == nil { - // Cannot check further, continue. - continue - } - - run := execRun.Run.Value - // https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun. - shell, err := fileparser.GetShellForStep(step, job) - if err != nil { - return false, err - } - // Skip unsupported shells. We don't support Windows shells or some Unix shells. - if !isSupportedShell(shell) { - continue - } - - // We replace the `${{ github.variable }}` to avoid shell parsing failures. - script := githubVarRegex.ReplaceAll([]byte(run), []byte("GITHUB_REDACTED_VAR")) - validated, err := validateShellFile(pathfn, uint(execRun.Run.Pos.Line), uint(execRun.Run.Pos.Line), - script, taintedFiles, dl) - if err != nil { - // Ignore parsing errors. - if errors.Is(err, sce.ErrorShellParsing) { - addPinnedResult(pdata, true) - } - return false, err - } - addPinnedResult(pdata, validated) - } - } - - return true, nil -} - -// Check pinning of github actions in workflows. -func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) { - var r worklowPinningResult - err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ - Pattern: ".github/workflows/*", - CaseSensitive: true, - }, validateGitHubActionWorkflow, c.Dlogger, &r) - return createReturnForIsGitHubActionsWorkflowPinned(r, c.Dlogger, err) -} - -// Create the result. -func createReturnForIsGitHubActionsWorkflowPinned(r worklowPinningResult, dl checker.DetailLogger, - err error, -) (int, error) { - return createReturnValuesForGitHubActionsWorkflowPinned(r, - "actions are pinned", - dl, err) -} - -func generateOwnerToDisplay(gitHubOwned bool) string { - if gitHubOwned { - return "GitHub-owned" - } - return "third-party" -} - -// validateGitHubActionWorkflow checks if the workflow file contains unpinned actions. Returns true if the check -// should continue executing after this file. -var validateGitHubActionWorkflow fileparser.DoWhileTrueOnFileContent = func( - pathfn string, - content []byte, - args ...interface{}, -) (bool, error) { - if !fileparser.IsWorkflowFile(pathfn) { - return true, nil - } - - if len(args) != 2 { - return false, fmt.Errorf( - "validateGitHubActionWorkflow requires exactly 2 arguments: %w", errInvalidArgLength) - } - pdata := dataAsWorkflowResultPointer(args[1]) - dl := dataAsDetailLogger(args[0]) - - if !fileparser.CheckFileContainsCommands(content, "#") { - addWorkflowPinnedResult(pdata, true, true) - addWorkflowPinnedResult(pdata, true, true) - return true, nil - } - - workflow, errs := actionlint.Parse(content) - if len(errs) > 0 && workflow == nil { - // actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards. - // Often we don't care about these errors. - return false, fileparser.FormatActionlintError(errs) - } - - hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) - for jobName, job := range workflow.Jobs { - jobName := jobName - job := job - if len(fileparser.GetJobName(job)) > 0 { - jobName = fileparser.GetJobName(job) - } - for _, step := range job.Steps { - if !fileparser.IsStepExecKind(step, actionlint.ExecKindAction) { - continue - } - execAction, ok := step.Exec.(*actionlint.ExecAction) - if !ok { - stepName := fileparser.GetStepName(step) - return false, sce.WithMessage(sce.ErrScorecardInternal, - fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName)) - } - - if execAction == nil || execAction.Uses == nil { - // Cannot check further, continue. - continue - } - - // nolint:lll - // Check whether this is an action defined in the same repo, - // https://docs.github.com/en/actions/learn-github-actions/finding-and-customizing-actions#referencing-an-action-in-the-same-repository-where-a-workflow-file-uses-the-action. - if strings.HasPrefix(execAction.Uses.Value, "./") { - continue - } - - // Check if we are dealing with a GitHub action or a third-party one. - gitHubOwned := fileparser.IsGitHubOwnedAction(execAction.Uses.Value) - owner := generateOwnerToDisplay(gitHubOwned) - - // Ensure a hash at least as large as SHA1 is used (40 hex characters). - // Example: action-name@hash - match := hashRegex.MatchString(execAction.Uses.Value) - if !match { - dl.Warn(&checker.LogMessage{ - Path: pathfn, Type: checker.FileTypeSource, - Offset: uint(execAction.Uses.Pos.Line), - EndOffset: uint(execAction.Uses.Pos.Line), // `Uses` always span a single line. - Snippet: execAction.Uses.Value, - Text: fmt.Sprintf("%s action not pinned by hash", owner), - Remediation: createWorkflowPinningRemediation(pathfn), - }) - } - - addWorkflowPinnedResult(pdata, match, gitHubOwned) - } - } - - return true, nil -} - -func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) { - if isGitHub { - addPinnedResult(&w.gitHubOwned, to) - } else { - addPinnedResult(&w.thirdParties, to) - } + return evaluation.PinningDependencies(CheckPinnedDependencies, c.Dlogger, &rawData) } diff --git a/checks/pinned_dependencies_test.go b/checks/pinned_dependencies_test.go deleted file mode 100644 index 862b6fa9763..00000000000 --- a/checks/pinned_dependencies_test.go +++ /dev/null @@ -1,1663 +0,0 @@ -// Copyright 2020 Security Scorecard Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package checks - -import ( - "errors" - "fmt" - "os" - "strings" - "testing" - - "github.com/ossf/scorecard/v4/checker" - scut "github.com/ossf/scorecard/v4/utests" -) - -func TestGithubWorkflowPinning(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "empty file", - filename: "./testdata/.github/workflows/github-workflow-empty.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "comments only", - filename: "./testdata/.github/workflows/github-workflow-comments.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Pinned workflow", - filename: "./testdata/.github/workflows/workflow-pinned.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Local action workflow", - filename: "./testdata/.github/workflows/workflow-local-action.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Non-pinned workflow", - filename: "./testdata/.github/workflows/workflow-not-pinned.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore - 2, - NumberOfWarn: 1, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "Non-yaml file", - filename: "./testdata/script.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Matrix as expression", - filename: "./testdata/.github/workflows/github-workflow-matrix-expression.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - - dl := scut.TestDetailLogger{} - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - s, e := testIsGitHubActionsWorkflowPinned(p, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestNonGithubWorkflowPinning(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "Pinned non-github workflow", - filename: "./testdata/.github/workflows/workflow-non-github-pinned.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Pinned github workflow", - filename: "./testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 2, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "Pinned github workflow", - filename: "./testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 2, - NumberOfDebug: 0, - }, - }, - { - name: "Mix of pinned and non-pinned GitHub actions", - filename: "./testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore - 2, - NumberOfWarn: 1, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "Mix of pinned and non-pinned non-GitHub actions", - filename: "./testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore - 8, - NumberOfWarn: 1, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - if tt.filename == "" { - content = make([]byte, 0) - } else { - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - } - dl := scut.TestDetailLogger{} - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - s, e := testIsGitHubActionsWorkflowPinned(p, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestGithubWorkflowPkgManagerPinning(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "npm packages without verification", - filename: "./testdata/.github/workflows/github-workflow-pkg-managers.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 28, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - - dl := scut.TestDetailLogger{} - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - s, e := testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(p, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestDockerfilePinning(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "invalid dockerfile", - filename: "./testdata/Dockerfile-invalid", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "invalid dockerfile sh", - filename: "./testdata/script-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "empty file", - filename: "./testdata/Dockerfile-empty", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "comments only", - filename: "./testdata/Dockerfile-comments", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "Pinned dockerfile", - filename: "./testdata/Dockerfile-pinned", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "Pinned dockerfile as", - filename: "./testdata/Dockerfile-pinned-as", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "Non-pinned dockerfile as", - filename: "./testdata/Dockerfile-not-pinned-as", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 2, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "Non-pinned dockerfile", - filename: "./testdata/Dockerfile-not-pinned", - expected: scut.TestReturn{ - Error: 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 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - if tt.filename == "" { - content = make([]byte, 0) - } else { - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - } - dl := scut.TestDetailLogger{} - s, e := testValidateDockerfileIsPinned(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestDockerfilePinningFromLineNumber(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected []struct { - snippet string - startLine uint - endLine uint - } - }{ - { - name: "Non-pinned dockerfile as", - filename: "./testdata/Dockerfile-not-pinned-as", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "FROM python:3.7 as build", - startLine: 17, - endLine: 17, - }, - { - snippet: "FROM build", - startLine: 23, - endLine: 23, - }, - }, - }, - { - name: "Non-pinned dockerfile", - filename: "./testdata/Dockerfile-not-pinned", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "FROM python:3.7", - startLine: 17, - endLine: 17, - }, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - var pinned pinnedResult - _, err = validateDockerfileIsPinned(tt.filename, content, &dl, &pinned) - if err != nil { - t.Errorf("error during validateDockerfileIsPinned: %v", err) - } - for _, expectedLog := range tt.expected { - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return logMessage.Offset == expectedLog.startLine && - logMessage.EndOffset == expectedLog.endLine && - logMessage.Path == tt.filename && - logMessage.Snippet == expectedLog.snippet && logType == checker.DetailWarn && - strings.Contains(logMessage.Text, "image not pinned by hash") - } - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - } - }) - } -} - -func TestDockerfileInvalidFiles(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected bool - }{ - { - name: "dockerfile go", - filename: "./testdata/Dockerfile.go", - expected: false, - }, - { - name: "dockerfile c", - filename: "./testdata/Dockerfile.c", - expected: false, - }, - { - name: "dockerfile cpp", - filename: "./testdata/Dockerfile.cpp", - expected: false, - }, - { - name: "dockerfile rust", - filename: "./testdata/Dockerfile.rs", - expected: false, - }, - { - name: "dockerfile js", - filename: "./testdata/Dockerfile.js", - expected: false, - }, - { - name: "dockerfile sh", - filename: "./testdata/Dockerfile.sh", - expected: false, - }, - { - name: "dockerfile py", - filename: "./testdata/Dockerfile.py", - expected: false, - }, - { - name: "dockerfile pyc", - filename: "./testdata/Dockerfile.pyc", - expected: false, - }, - { - name: "dockerfile java", - filename: "./testdata/Dockerfile.java", - expected: false, - }, - { - name: "dockerfile ", - filename: "./testdata/Dockerfile.any", - expected: true, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var c []byte - r := isDockerfile(tt.filename, c) - if r != tt.expected { - t.Errorf("test failed: %s. Expected %v. Got %v", tt.filename, r, tt.expected) - } - }) - } -} - -func TestDockerfileInsecureDownloadsLineNumber(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected []struct { - snippet string - startLine uint - endLine uint - } - }{ - { - name: "dockerfile downloads", - filename: "./testdata/Dockerfile-download-lines", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "curl bla | bash", - startLine: 35, - endLine: 36, - }, - { - snippet: "pip install -r requirements.txt", - startLine: 41, - endLine: 42, - }, - }, - }, - { - name: "dockerfile downloads multi-run", - filename: "./testdata/Dockerfile-download-multi-runs", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "/tmp/file3", - startLine: 28, - endLine: 28, - }, - { - snippet: "/tmp/file1", - startLine: 30, - endLine: 30, - }, - { - snippet: "bash /tmp/file3", - startLine: 32, - endLine: 34, - }, - { - snippet: "bash /tmp/file1", - startLine: 37, - endLine: 38, - }, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - var pinned pinnedResult - _, err = validateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl, &pinned) - if err != nil { - t.Errorf("error during validateDockerfileIsPinned: %v", err) - } - - for _, expectedLog := range tt.expected { - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - fmt.Println(logMessage) - return logMessage.Offset == expectedLog.startLine && - logMessage.EndOffset == expectedLog.endLine && - logMessage.Path == tt.filename && - logMessage.Snippet == expectedLog.snippet && logType == checker.DetailWarn - } - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - } - }) - } -} - -func TestShellscriptInsecureDownloadsLineNumber(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected []struct { - snippet string - startLine uint - endLine uint - } - }{ - { - name: "shell downloads", - filename: "./testdata/shell-download-lines.sh", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "bash /tmp/file", - startLine: 6, - endLine: 6, - }, - { - snippet: "curl bla | bash", - startLine: 11, - endLine: 11, - }, - { - snippet: "bash <(wget -qO- http://website.com/my-script.sh)", - startLine: 18, - endLine: 18, - }, - { - snippet: "bash <(wget -qO- http://website.com/my-script.sh)", - startLine: 20, - endLine: 20, - }, - { - snippet: "pip install -r requirements.txt", - startLine: 26, - endLine: 26, - }, - { - snippet: "curl bla | bash", - startLine: 28, - endLine: 28, - }, - { - snippet: "choco install 'some-package'", - startLine: 30, - endLine: 30, - }, - { - snippet: "choco install 'some-other-package'", - startLine: 31, - endLine: 31, - }, - }, - }, - } - - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - var pinned pinnedResult - _, err = validateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl, &pinned) - if err != nil { - t.Errorf("error during validateDockerfileIsPinned: %v", err) - } - - for _, expectedLog := range tt.expected { - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return logMessage.Offset == expectedLog.startLine && - logMessage.EndOffset == expectedLog.endLine && - logMessage.Path == tt.filename && - logMessage.Snippet == expectedLog.snippet && logType == checker.DetailWarn - } - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - } - }) - } -} - -func TestDockerfilePinningWihoutHash(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "Pinned dockerfile as no hash", - filename: "./testdata/Dockerfile-pinned-as-without-hash", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 4, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "Dockerfile with args", - filename: "./testdata/Dockerfile-args", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 2, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "Dockerfile with base", - filename: "./testdata/Dockerfile-base", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - s, e := testValidateDockerfileIsPinned(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - if tt.expected.NumberOfWarn > 0 { - return strings.Contains(logMessage.Text, "image not pinned by hash") - } - return true - } - - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - }) - } -} - -func TestDockerfileScriptDownload(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "curl | sh", - filename: "./testdata/Dockerfile-curl-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 4, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "empty file", - filename: "./testdata/Dockerfile-empty", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "invalid file sh", - filename: "./testdata/script.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "comments only", - filename: "./testdata/Dockerfile-comments", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "wget | /bin/sh", - filename: "./testdata/Dockerfile-wget-bin-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 3, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "wget no exec", - filename: "./testdata/Dockerfile-script-ok", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "curl file sh", - filename: "./testdata/Dockerfile-curl-file-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 12, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "proc substitution", - filename: "./testdata/Dockerfile-proc-subs", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 6, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "wget file", - filename: "./testdata/Dockerfile-wget-file", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 10, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "gsutil file", - filename: "./testdata/Dockerfile-gsutil-file", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 17, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "aws file", - filename: "./testdata/Dockerfile-aws-file", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 15, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "pkg managers", - filename: "./testdata/Dockerfile-pkg-managers", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 39, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "download with some python", - filename: "./testdata/Dockerfile-some-python", - expected: scut.TestReturn{ - Error: 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 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - if tt.filename == "" { - content = make([]byte, 0) - } else { - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - } - dl := scut.TestDetailLogger{} - s, e := testValidateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestDockerfileScriptDownloadInfo(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "curl | sh", - filename: "./testdata/Dockerfile-no-curl-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - s, e := testValidateDockerfileIsFreeOfInsecureDownloads(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return strings.Contains(logMessage.Text, - "no insecure (not pinned by hash) dependency downloads found in Dockerfiles") - } - - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Fail() - } - }) - } -} - -func TestShellScriptDownload(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "sh script", - filename: "./testdata/script-sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 7, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "empty file", - filename: "./testdata/script-empty.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "comments", - filename: "./testdata/script-comments.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "bash script", - filename: "./testdata/script-bash", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 7, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "sh script 2", - filename: "./testdata/script.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 7, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "pkg managers", - filename: "./testdata/script-pkg-managers", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 36, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "invalid shell script", - filename: "./testdata/script-invalid.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 1, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - if tt.filename == "" { - content = make([]byte, 0) - } else { - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - } - dl := scut.TestDetailLogger{} - s, e := testValidateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestShellScriptDownloadPinned(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "sh script", - filename: "./testdata/script-comments.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - { - name: "script free of download", - filename: "./testdata/script-free-from-download.sh", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MaxResultScore, - NumberOfWarn: 0, - NumberOfInfo: 1, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - - dl := scut.TestDetailLogger{} - s, e := testValidateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return strings.Contains(logMessage.Text, - "no insecure (not pinned by hash) dependency downloads found in shell scripts") - } - - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Fail() - } - }) - } -} - -func TestGitHubWorflowRunDownload(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected scut.TestReturn - }{ - { - name: "workflow curl default", - filename: "./testdata/.github/workflows/github-workflow-curl-default.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 1, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "workflow curl no default", - filename: "./testdata/.github/workflows/github-workflow-curl-no-default.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 1, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - { - name: "wget across steps", - filename: "./testdata/.github/workflows/github-workflow-wget-across-steps.yaml", - expected: scut.TestReturn{ - Error: nil, - Score: checker.MinResultScore, - NumberOfWarn: 2, - NumberOfInfo: 0, - NumberOfDebug: 0, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var content []byte - var err error - if tt.filename == "" { - content = make([]byte, 0) - } else { - content, err = os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - } - dl := scut.TestDetailLogger{} - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - s, e := testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(p, content, &dl) - actual := checker.CheckResult{ - Score: s, - Error: e, - } - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func TestGitHubWorkflowUsesLineNumber(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected []struct { - dependency string - startLine uint - endLine uint - } - }{ - { - name: "unpinned dependency in uses", - filename: "./testdata/.github/workflows/github-workflow-permissions-run-codeql-write.yaml", - expected: []struct { - dependency string - startLine uint - endLine uint - }{ - { - dependency: "github/codeql-action/analyze@v1", - startLine: 25, - endLine: 25, - }, - }, - }, - { - name: "multiple unpinned dependency in uses", - filename: "./testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml", - expected: []struct { - dependency string - startLine uint - endLine uint - }{ - { - dependency: "github/codeql-action/analyze@v1", - startLine: 22, - endLine: 22, - }, - { - dependency: "docker/build-push-action@1.2.3", - startLine: 24, - endLine: 24, - }, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - var pinned worklowPinningResult - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - _, err = validateGitHubActionWorkflow(p, content, &dl, &pinned) - if err != nil { - t.Errorf("error during validateGitHubActionWorkflow: %v", err) - } - for _, expectedLog := range tt.expected { - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return logMessage.Offset == expectedLog.startLine && - logMessage.EndOffset == expectedLog.endLine && - logMessage.Path == p && - logMessage.Snippet == expectedLog.dependency && logType == checker.DetailWarn && - strings.Contains(logMessage.Text, "action not pinned by hash") - } - - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - } - }) - } -} - -func TestGitHubWorkInsecureDownloadsLineNumber(t *testing.T) { - t.Parallel() - tests := []struct { - name string - filename string - expected []struct { - snippet string - startLine uint - endLine uint - } - }{ - { - name: "downloads", - filename: "./testdata/.github/workflows/github-workflow-download-lines.yaml", - expected: []struct { - snippet string - startLine uint - endLine uint - }{ - { - snippet: "bash /tmp/file", - startLine: 27, - endLine: 27, - }, - { - snippet: "/tmp/file2", - startLine: 29, - endLine: 29, - }, - { - snippet: "curl bla | bash", - startLine: 32, - endLine: 32, - }, - }, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := os.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - dl := scut.TestDetailLogger{} - var pinned pinnedResult - p := strings.Replace(tt.filename, "./testdata/", "", 1) - - _, err = validateGitHubWorkflowIsFreeOfInsecureDownloads(p, content, &dl, &pinned) - if err != nil { - t.Errorf("error during validateGitHubWorkflowIsFreeOfInsecureDownloads: %v", err) - } - for _, expectedLog := range tt.expected { - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return logMessage.Offset == expectedLog.startLine && - logMessage.EndOffset == expectedLog.endLine && - logMessage.Path == p && - logMessage.Snippet == expectedLog.snippet && logType == checker.DetailWarn - } - - if !scut.ValidateLogMessage(isExpectedLog, &dl) { - t.Errorf("test failed: log message not present: %+v", tt.expected) - } - } - }) - } -} - -func Test_createReturnValuesForGitHubActionsWorkflowPinned(t *testing.T) { - t.Parallel() - //nolint - type args struct { - r worklowPinningResult - infoMsg string - dl checker.DetailLogger - err error - } - //nolint - tests := []struct { - name string - args args - want int - wantErr bool - }{ - { - name: "both actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 1, - gitHubOwned: 1, - }, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - err: nil, - }, - want: 10, - wantErr: false, - }, - { - name: "github actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 2, - gitHubOwned: 2, - }, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - err: nil, - }, - want: 0, - wantErr: false, - }, - { - name: "error in github actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 2, - gitHubOwned: 2, - }, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - err: errors.New("error"), - }, - want: -1, - wantErr: true, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := createReturnValuesForGitHubActionsWorkflowPinned(tt.args.r, tt.args.infoMsg, tt.args.dl, tt.args.err) - if (err != nil) != tt.wantErr { - t.Errorf("createReturnValuesForGitHubActionsWorkflowPinned() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("createReturnValuesForGitHubActionsWorkflowPinned() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_createReturnValues(t *testing.T) { - t.Parallel() - //nolint - type args struct { - r pinnedResult - infoMsg string - dl checker.DetailLogger - err error - } - //nolint - tests := []struct { - name string - args args - want int - wantErr bool - }{ - { - name: "returns 10 if no error", - args: args{ - r: 1, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - err: nil, - }, - want: 10, - wantErr: false, - }, - { - name: "returns 0 if unpinned ", - args: args{ - r: 2, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - err: nil, - }, - want: 0, - wantErr: false, - }, - { - name: "if err is not nil, returns 0", - args: args{ - r: 2, - infoMsg: "", - dl: &scut.TestDetailLogger{}, - //nolint - err: errors.New("error"), - }, - want: -1, - wantErr: true, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := createReturnValues(tt.args.r, tt.args.infoMsg, tt.args.dl, tt.args.err) - if (err != nil) != tt.wantErr { - t.Errorf("createReturnValues() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("createReturnValues() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_maxScore(t *testing.T) { - t.Parallel() - type args struct { - s1 int - s2 int - } - tests := []struct { - name string - args args - want int - }{ - { - name: "returns s1 if s1 is greater than s2", - args: args{ - s1: 10, - s2: 5, - }, - want: 10, - }, - { - name: "returns s2 if s2 is greater than s1", - args: args{ - s1: 5, - s2: 10, - }, - want: 10, - }, - { - name: "returns s1 if s1 is equal to s2", - args: args{ - s1: 10, - s2: 10, - }, - want: 10, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := maxScore(tt.args.s1, tt.args.s2); got != tt.want { - t.Errorf("maxScore() = %v, want %v", got, tt.want) - } - }) - } -} - -func testValidateShellScriptIsFreeOfInsecureDownloads(pathfn string, - content []byte, dl checker.DetailLogger, -) (int, error) { - var r pinnedResult - _, err := validateShellScriptIsFreeOfInsecureDownloads(pathfn, content, dl, &r) - return createReturnForIsShellScriptFreeOfInsecureDownloads(r, dl, err) -} - -func testValidateDockerfileIsFreeOfInsecureDownloads(pathfn string, - content []byte, dl checker.DetailLogger, -) (int, error) { - var r pinnedResult - _, err := validateDockerfileIsFreeOfInsecureDownloads(pathfn, content, dl, &r) - return createReturnForIsDockerfileFreeOfInsecureDownloads(r, dl, err) -} - -func testValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.DetailLogger) (int, error) { - var r pinnedResult - _, err := validateDockerfileIsPinned(pathfn, content, dl, &r) - return createReturnForIsDockerfilePinned(r, dl, err) -} - -func testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, - content []byte, dl checker.DetailLogger, -) (int, error) { - var r pinnedResult - _, err := validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn, content, dl, &r) - return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, dl, err) -} - -func testIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker.DetailLogger) (int, error) { - var r worklowPinningResult - _, err := validateGitHubActionWorkflow(pathfn, content, dl, &r) - return createReturnForIsGitHubActionsWorkflowPinned(r, dl, err) -} diff --git a/checks/raw/errors.go b/checks/raw/errors.go index 0de31a67d41..d01c31277c3 100644 --- a/checks/raw/errors.go +++ b/checks/raw/errors.go @@ -19,8 +19,9 @@ import ( ) var ( - errInternalCommitishNil = errors.New("commitish is nil") - errInvalidArgType = errors.New("invalid arg type") - errInvalidArgLength = errors.New("invalid arg length") - errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow") + errInternalInvalidDockerFile = errors.New("invalid Dockerfile") + errInternalCommitishNil = errors.New("commitish is nil") + errInvalidArgType = errors.New("invalid arg type") + errInvalidArgLength = errors.New("invalid arg length") + errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow") ) diff --git a/checks/raw/pinned_dependencies.go b/checks/raw/pinned_dependencies.go new file mode 100644 index 00000000000..3465b1940a7 --- /dev/null +++ b/checks/raw/pinned_dependencies.go @@ -0,0 +1,499 @@ +// Copyright 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 raw + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/moby/buildkit/frontend/dockerfile/parser" + "github.com/rhysd/actionlint" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/checks/fileparser" + sce "github.com/ossf/scorecard/v4/errors" +) + +// PinningDependencies checks for (un)pinned dependencies. +func PinningDependencies(c *checker.CheckRequest) (checker.PinningDependenciesData, error) { + var results checker.PinningDependenciesData + // GitHub actions. + if err := collectGitHubActionsWorkflowPinning(c, &results); err != nil { + return checker.PinningDependenciesData{}, err + } + + // // Docker files. + if err := collectDockerfilePinning(c, &results); err != nil { + return checker.PinningDependenciesData{}, err + } + + // Docker downloads. + if err := collectDockerfileInsecureDownloads(c, &results); err != nil { + return checker.PinningDependenciesData{}, err + } + + // Script downloads. + if err := collectShellScriptInsecureDownloads(c, &results); err != nil { + return checker.PinningDependenciesData{}, err + } + + // Action script downloads. + if err := collectGitHubWorkflowScriptInsecureDownloads(c, &results); err != nil { + return checker.PinningDependenciesData{}, err + } + + return results, nil +} + +func dataAsPinnedDependenciesPointer(data interface{}) *checker.PinningDependenciesData { + pdata, ok := data.(*checker.PinningDependenciesData) + if !ok { + // panic if it is not correct type + panic(fmt.Sprintf("expected type PinningDependenciesData, got %v", reflect.TypeOf(data))) + } + return pdata +} + +func collectShellScriptInsecureDownloads(c *checker.CheckRequest, r *checker.PinningDependenciesData) error { + return fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ + Pattern: "*", + CaseSensitive: false, + }, validateShellScriptIsFreeOfInsecureDownloads, r) +} + +var validateShellScriptIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( + pathfn string, + content []byte, + args ...interface{}, +) (bool, error) { + if len(args) != 1 { + return false, fmt.Errorf( + "validateShellScriptIsFreeOfInsecureDownloads requires exactly 1 arguments: got %v: %w", + len(args), errInvalidArgLength) + } + + pdata := dataAsPinnedDependenciesPointer(args[0]) + + // Validate the file type. + if !isSupportedShellScriptFile(pathfn, content) { + return true, nil + } + + if err := validateShellFile(pathfn, 0, 0, content, map[string]bool{}, pdata); err != nil { + return false, nil + } + + return true, nil +} + +func collectDockerfileInsecureDownloads(c *checker.CheckRequest, r *checker.PinningDependenciesData) error { + return fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ + Pattern: "*Dockerfile*", + CaseSensitive: false, + }, validateDockerfileInsecureDownloads, r) +} + +var validateDockerfileInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( + pathfn string, + content []byte, + args ...interface{}, +) (bool, error) { + if len(args) != 1 { + return false, fmt.Errorf( + "validateDockerfileInsecureDownloads requires exactly 1 arguments: got %v: %w", + len(args), errInvalidArgLength) + } + + pdata := dataAsPinnedDependenciesPointer(args[0]) + + // Return early if this is not a docker file. + if !isDockerfile(pathfn, content) { + return true, nil + } + + if !fileparser.CheckFileContainsCommands(content, "#") { + return true, nil + } + + contentReader := strings.NewReader(string(content)) + res, err := parser.Parse(contentReader) + if err != nil { + return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("%v: %v", errInternalInvalidDockerFile, err)) + } + + // Walk the Dockerfile's AST. + taintedFiles := make(map[string]bool) + for i := range res.AST.Children { + var bytes []byte + + child := res.AST.Children[i] + cmdType := child.Value + + // Only look for the 'RUN' command. + if cmdType != "RUN" { + continue + } + + var valueList []string + for n := child.Next; n != nil; n = n.Next { + valueList = append(valueList, n.Value) + } + + if len(valueList) == 0 { + return false, sce.WithMessage(sce.ErrScorecardInternal, errInternalInvalidDockerFile.Error()) + } + + // Build a file content. + cmd := strings.Join(valueList, " ") + bytes = append(bytes, cmd...) + if err := validateShellFile(pathfn, uint(child.StartLine)-1, uint(child.EndLine)-1, + bytes, taintedFiles, pdata); err != nil { + return false, err + } + } + + return true, nil +} + +func isDockerfile(pathfn string, content []byte) bool { + if strings.HasSuffix(pathfn, ".go") || + strings.HasSuffix(pathfn, ".c") || + strings.HasSuffix(pathfn, ".cpp") || + strings.HasSuffix(pathfn, ".rs") || + strings.HasSuffix(pathfn, ".js") || + strings.HasSuffix(pathfn, ".py") || + strings.HasSuffix(pathfn, ".pyc") || + strings.HasSuffix(pathfn, ".java") || + isShellScriptFile(pathfn, content) { + return false + } + return true +} + +func collectDockerfilePinning(c *checker.CheckRequest, r *checker.PinningDependenciesData) error { + return fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ + Pattern: "*Dockerfile*", + CaseSensitive: false, + }, validateDockerfilesPinning, r) +} + +var validateDockerfilesPinning fileparser.DoWhileTrueOnFileContent = func( + pathfn string, + content []byte, + args ...interface{}, +) (bool, error) { + // Users may use various names, e.g., + // Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template + + if len(args) != 1 { + return false, fmt.Errorf( + "validateDockerfilesPinning requires exactly 2 arguments: got %v: %w", len(args), errInvalidArgLength) + } + pdata := dataAsPinnedDependenciesPointer(args[0]) + + // Return early if this is not a dockerfile. + if !isDockerfile(pathfn, content) { + return true, nil + } + + if !fileparser.CheckFileContainsCommands(content, "#") { + return true, nil + } + + if fileparser.IsTemplateFile(pathfn) { + 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)) + // The dependency must be pinned by sha256 hash, e.g., + // FROM something@sha256:${ARG}, + // FROM something:@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + regex := regexp.MustCompile(`.*@sha256:([a-f\d]{64}|\${.*})`) + + pinnedAsNames := make(map[string]bool) + res, err := parser.Parse(contentReader) + if err != nil { + return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("%v: %v", errInternalInvalidDockerFile, err)) + } + + for _, child := range res.AST.Children { + cmdType := child.Value + if cmdType != "FROM" { + continue + } + + var valueList []string + for n := child.Next; n != nil; n = n.Next { + valueList = append(valueList, n.Value) + } + + switch { + // scratch is no-op. + case len(valueList) > 0 && strings.EqualFold(valueList[0], "scratch"): + continue + + // FROM name AS newname. + case len(valueList) == 3 && strings.EqualFold(valueList[1], "as"): + name := valueList[0] + asName := valueList[2] + // Check if the name is pinned. + // (1): name = <>@sha245:hash + // (2): name = XXX where XXX was pinned + pinned := pinnedAsNames[name] + if pinned || regex.MatchString(name) { + // Record the asName. + pinnedAsNames[asName] = true + continue + } + + pdata.Dependencies = append(pdata.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: uint(child.StartLine), + EndOffset: uint(child.EndLine), + Snippet: child.Original, + }, + Name: asPointer(name), + PinnedAt: asPointer(asName), + Type: checker.DependencyUseTypeDockerfileContainerImage, + }, + ) + + // FROM name. + case len(valueList) == 1: + name := valueList[0] + pinned := pinnedAsNames[name] + if !pinned && !regex.MatchString(name) { + dep := checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: uint(child.StartLine), + EndOffset: uint(child.EndLine), + Snippet: child.Original, + }, + Type: checker.DependencyUseTypeDockerfileContainerImage, + } + parts := strings.SplitN(name, ":", 2) + if len(parts) > 0 { + dep.Name = asPointer(parts[0]) + if len(parts) > 1 { + dep.PinnedAt = asPointer(parts[1]) + } + } + pdata.Dependencies = append(pdata.Dependencies, dep) + } + + default: + // That should not happen. + return false, sce.WithMessage(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 true, nil +} + +func collectGitHubWorkflowScriptInsecureDownloads(c *checker.CheckRequest, r *checker.PinningDependenciesData) error { + return fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ + Pattern: ".github/workflows/*", + CaseSensitive: false, + }, validateGitHubWorkflowIsFreeOfInsecureDownloads, r) +} + +// validateGitHubWorkflowIsFreeOfInsecureDownloads checks if the workflow file downloads dependencies that are unpinned. +// Returns true if the check should continue executing after this file. +var validateGitHubWorkflowIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func( + pathfn string, + content []byte, + args ...interface{}, +) (bool, error) { + if !fileparser.IsWorkflowFile(pathfn) { + return true, nil + } + + if len(args) != 1 { + return false, fmt.Errorf( + "validateGitHubWorkflowIsFreeOfInsecureDownloads requires exactly 1 arguments: got %v: %w", + len(args), errInvalidArgLength) + } + + pdata := dataAsPinnedDependenciesPointer(args[0]) + if !fileparser.CheckFileContainsCommands(content, "#") { + return true, nil + } + + workflow, errs := actionlint.Parse(content) + if len(errs) > 0 && workflow == nil { + // actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards. + // Often we don't care about these errors. + return false, fileparser.FormatActionlintError(errs) + } + + githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`) + for jobName, job := range workflow.Jobs { + jobName := jobName + job := job + if len(fileparser.GetJobName(job)) > 0 { + jobName = fileparser.GetJobName(job) + } + taintedFiles := make(map[string]bool) + for _, step := range job.Steps { + step := step + if !fileparser.IsStepExecKind(step, actionlint.ExecKindRun) { + continue + } + + execRun, ok := step.Exec.(*actionlint.ExecRun) + if !ok { + stepName := fileparser.GetStepName(step) + return false, sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName)) + } + + if execRun == nil || execRun.Run == nil { + // Cannot check further, continue. + continue + } + + run := execRun.Run.Value + // https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun. + shell, err := fileparser.GetShellForStep(step, job) + if err != nil { + return false, err + } + // Skip unsupported shells. We don't support Windows shells or some Unix shells. + if !isSupportedShell(shell) { + continue + } + + // We replace the `${{ github.variable }}` to avoid shell parsing failures. + script := githubVarRegex.ReplaceAll([]byte(run), []byte("GITHUB_REDACTED_VAR")) + if err := validateShellFile(pathfn, uint(execRun.Run.Pos.Line), uint(execRun.Run.Pos.Line), + script, taintedFiles, pdata); err != nil { + pdata.Dependencies = append(pdata.Dependencies, checker.Dependency{ + Msg: asPointer(err.Error()), + }) + } + } + } + + return true, nil +} + +// Check pinning of github actions in workflows. +func collectGitHubActionsWorkflowPinning(c *checker.CheckRequest, r *checker.PinningDependenciesData) error { + return fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{ + Pattern: ".github/workflows/*", + CaseSensitive: true, + }, validateGitHubActionWorkflow, r) +} + +// validateGitHubActionWorkflow checks if the workflow file contains unpinned actions. Returns true if the check +// should continue executing after this file. +var validateGitHubActionWorkflow fileparser.DoWhileTrueOnFileContent = func( + pathfn string, + content []byte, + args ...interface{}, +) (bool, error) { + if !fileparser.IsWorkflowFile(pathfn) { + return true, nil + } + + if len(args) != 1 { + return false, fmt.Errorf( + "validateGitHubActionWorkflow requires exactly 1 arguments: got %v: %w", len(args), errInvalidArgLength) + } + pdata := dataAsPinnedDependenciesPointer(args[0]) + + if !fileparser.CheckFileContainsCommands(content, "#") { + return true, nil + } + + workflow, errs := actionlint.Parse(content) + if len(errs) > 0 && workflow == nil { + // actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards. + // Often we don't care about these errors. + return false, fileparser.FormatActionlintError(errs) + } + + hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`) + for jobName, job := range workflow.Jobs { + jobName := jobName + job := job + if len(fileparser.GetJobName(job)) > 0 { + jobName = fileparser.GetJobName(job) + } + for _, step := range job.Steps { + if !fileparser.IsStepExecKind(step, actionlint.ExecKindAction) { + continue + } + + execAction, ok := step.Exec.(*actionlint.ExecAction) + if !ok { + stepName := fileparser.GetStepName(step) + return false, sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName)) + } + + if execAction == nil || execAction.Uses == nil { + // Cannot check further, continue. + continue + } + + // nolint:lll + // Check whether this is an action defined in the same repo, + // https://docs.github.com/en/actions/learn-github-actions/finding-and-customizing-actions#referencing-an-action-in-the-same-repository-where-a-workflow-file-uses-the-action. + if strings.HasPrefix(execAction.Uses.Value, "./") { + continue + } + + // Ensure a hash at least as large as SHA1 is used (40 hex characters). + // Example: action-name@hash + match := hashRegex.MatchString(execAction.Uses.Value) + if !match { + dep := checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: uint(execAction.Uses.Pos.Line), + EndOffset: uint(execAction.Uses.Pos.Line), // `Uses` always span a single line. + Snippet: execAction.Uses.Value, + }, + Type: checker.DependencyUseTypeGHAction, + } + parts := strings.SplitN(execAction.Uses.Value, "@", 2) + if len(parts) > 0 { + dep.Name = asPointer(parts[0]) + if len(parts) > 1 { + dep.PinnedAt = asPointer(parts[1]) + } + } + pdata.Dependencies = append(pdata.Dependencies, dep) + } + } + } + + return true, nil +} diff --git a/checks/raw/pinned_dependencies_test.go b/checks/raw/pinned_dependencies_test.go new file mode 100644 index 00000000000..58efddb7be4 --- /dev/null +++ b/checks/raw/pinned_dependencies_test.go @@ -0,0 +1,1192 @@ +// Copyright 2020 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package raw + +import ( + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ossf/scorecard/v4/checker" + scut "github.com/ossf/scorecard/v4/utests" +) + +func TestGithubWorkflowPinning(t *testing.T) { + t.Parallel() + + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "empty file", + filename: "./testdata/.github/workflows/github-workflow-empty.yaml", + }, + { + name: "comments only", + filename: "./testdata/.github/workflows/github-workflow-comments.yaml", + }, + { + name: "Pinned workflow", + filename: "./testdata/.github/workflows/workflow-pinned.yaml", + }, + { + name: "Local action workflow", + filename: "./testdata/.github/workflows/workflow-local-action.yaml", + }, + { + name: "Non-pinned workflow", + filename: "./testdata/.github/workflows/workflow-not-pinned.yaml", + warns: 1, + }, + { + name: "Non-yaml file", + filename: "../testdata/script.sh", + }, + { + name: "Matrix as expression", + filename: "./testdata/.github/workflows/github-workflow-matrix-expression.yaml", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + p := strings.Replace(tt.filename, "./testdata/", "", 1) + p = strings.Replace(p, "../testdata/", "", 1) + + var r checker.PinningDependenciesData + + _, err = validateGitHubActionWorkflow(p, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestNonGithubWorkflowPinning(t *testing.T) { + t.Parallel() + + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "Pinned non-github workflow", + filename: "./testdata/.github/workflows/workflow-non-github-pinned.yaml", + }, + { + name: "Pinned github workflow", + filename: "./testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml", + warns: 2, + }, + { + name: "Pinned github workflow", + filename: "./testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml", + }, + { + name: "Mix of pinned and non-pinned GitHub actions", + filename: "./testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml", + warns: 1, + }, + { + name: "Mix of pinned and non-pinned non-GitHub actions", + filename: "./testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml", + warns: 1, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + if tt.filename == "" { + content = make([]byte, 0) + } else { + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + } + + p := strings.Replace(tt.filename, "./testdata/", "", 1) + var r checker.PinningDependenciesData + + _, err = validateGitHubActionWorkflow(p, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestGithubWorkflowPkgManagerPinning(t *testing.T) { + t.Parallel() + + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "npm packages without verification", + filename: "./testdata/.github/workflows/github-workflow-pkg-managers.yaml", + warns: 28, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + p := strings.Replace(tt.filename, "./testdata/", "", 1) + var r checker.PinningDependenciesData + + _, err = validateGitHubWorkflowIsFreeOfInsecureDownloads(p, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestDockerfilePinning(t *testing.T) { + t.Parallel() + + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "invalid dockerfile", + filename: "./testdata/Dockerfile-invalid", + }, + { + name: "invalid dockerfile sh", + filename: "../testdata/script-sh", + }, + { + name: "empty file", + filename: "./testdata/Dockerfile-empty", + }, + { + name: "comments only", + filename: "./testdata/Dockerfile-comments", + }, + { + name: "Pinned dockerfile", + filename: "./testdata/Dockerfile-pinned", + }, + { + name: "Pinned dockerfile as", + filename: "./testdata/Dockerfile-pinned-as", + }, + { + name: "Non-pinned dockerfile as", + filename: "./testdata/Dockerfile-not-pinned-as", + warns: 2, + }, + { + name: "Non-pinned dockerfile", + filename: "./testdata/Dockerfile-not-pinned", + warns: 1, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + if tt.filename == "" { + content = make([]byte, 0) + } else { + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + } + + var r checker.PinningDependenciesData + _, err = validateDockerfilesPinning(tt.filename, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestDockerfilePinningFromLineNumber(t *testing.T) { + t.Parallel() + tests := []struct { + name string + filename string + expected []struct { + snippet string + startLine uint + endLine uint + } + }{ + { + name: "Non-pinned dockerfile as", + filename: "./testdata/Dockerfile-not-pinned-as", + expected: []struct { + snippet string + startLine uint + endLine uint + }{ + { + snippet: "FROM python:3.7 as build", + startLine: 17, + endLine: 17, + }, + { + snippet: "FROM build", + startLine: 23, + endLine: 23, + }, + }, + }, + { + name: "Non-pinned dockerfile", + filename: "./testdata/Dockerfile-not-pinned", + expected: []struct { + snippet string + startLine uint + endLine uint + }{ + { + snippet: "FROM python:3.7", + startLine: 17, + endLine: 17, + }, + }, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + var r checker.PinningDependenciesData + _, err = validateDockerfilesPinning(tt.filename, content, &r) + if err != nil { + t.Errorf("error during validateDockerfilesPinning: %v", err) + } + + for _, expectedDep := range tt.expected { + isExpectedDep := func(dep checker.Dependency) bool { + return dep.Location.Offset == expectedDep.startLine && + dep.Location.EndOffset == expectedDep.endLine && + dep.Location.Path == tt.filename && + dep.Location.Snippet == expectedDep.snippet && + dep.Type == checker.DependencyUseTypeDockerfileContainerImage + } + + if !scut.ValidatePinningDependencies(isExpectedDep, &r) { + t.Errorf("test failed: dependency not present: %+v", tt.expected) + } + } + }) + } +} + +func TestDockerfileInvalidFiles(t *testing.T) { + t.Parallel() + tests := []struct { + name string + filename string + expected bool + }{ + { + name: "dockerfile go", + filename: "./testdata/Dockerfile.go", + expected: false, + }, + { + name: "dockerfile c", + filename: "./testdata/Dockerfile.c", + expected: false, + }, + { + name: "dockerfile cpp", + filename: "./testdata/Dockerfile.cpp", + expected: false, + }, + { + name: "dockerfile rust", + filename: "./testdata/Dockerfile.rs", + expected: false, + }, + { + name: "dockerfile js", + filename: "./testdata/Dockerfile.js", + expected: false, + }, + { + name: "dockerfile sh", + filename: "./testdata/Dockerfile.sh", + expected: false, + }, + { + name: "dockerfile py", + filename: "./testdata/Dockerfile.py", + expected: false, + }, + { + name: "dockerfile pyc", + filename: "./testdata/Dockerfile.pyc", + expected: false, + }, + { + name: "dockerfile java", + filename: "./testdata/Dockerfile.java", + expected: false, + }, + { + name: "dockerfile ", + filename: "./testdata/Dockerfile.any", + expected: true, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var c []byte + r := isDockerfile(tt.filename, c) + if r != tt.expected { + t.Errorf("test failed: %s. Expected %v. Got %v", tt.filename, r, tt.expected) + } + }) + } +} + +func TestDockerfileInsecureDownloadsLineNumber(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + expected []struct { + snippet string + startLine uint + endLine uint + t checker.DependencyUseType + } + }{ + { + name: "dockerfile downloads", + filename: "./testdata/Dockerfile-download-lines", + //nolint + expected: []struct { + snippet string + startLine uint + endLine uint + t checker.DependencyUseType + }{ + { + snippet: "curl bla | bash", + startLine: 35, + endLine: 36, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "pip install -r requirements.txt", + startLine: 41, + endLine: 42, + t: checker.DependencyUseTypePipCommand, + }, + }, + }, + { + name: "dockerfile downloads multi-run", + filename: "./testdata/Dockerfile-download-multi-runs", + //nolint + expected: []struct { + snippet string + startLine uint + endLine uint + t checker.DependencyUseType + }{ + { + snippet: "/tmp/file3", + startLine: 28, + endLine: 28, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "/tmp/file1", + startLine: 30, + endLine: 30, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "bash /tmp/file3", + startLine: 32, + endLine: 34, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "bash /tmp/file1", + startLine: 37, + endLine: 38, + t: checker.DependencyUseTypeDownloadThenRun, + }, + }, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + var r checker.PinningDependenciesData + _, err = validateDockerfileInsecureDownloads(tt.filename, content, &r) + if err != nil { + t.Errorf("error during validateDockerfileInsecureDownloads: %v", err) + } + + for _, expectedDep := range tt.expected { + isExpectedDep := func(dep checker.Dependency) bool { + return dep.Location.Offset == expectedDep.startLine && + dep.Location.EndOffset == expectedDep.endLine && + dep.Location.Path == tt.filename && + dep.Location.Snippet == expectedDep.snippet && + dep.Type == expectedDep.t + } + + if !scut.ValidatePinningDependencies(isExpectedDep, &r) { + t.Errorf("test failed: dependency not present: %+v", tt.expected) + } + } + }) + } +} + +func TestShellscriptInsecureDownloadsLineNumber(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + expected []struct { + snippet string + startLine uint + endLine uint + t checker.DependencyUseType + } + }{ + { + name: "shell downloads", + filename: "./testdata/shell-download-lines.sh", + //nolint + expected: []struct { + snippet string + startLine uint + endLine uint + t checker.DependencyUseType + }{ + { + snippet: "bash /tmp/file", + startLine: 6, + endLine: 6, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "curl bla | bash", + startLine: 11, + endLine: 11, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "bash <(wget -qO- http://website.com/my-script.sh)", + startLine: 18, + endLine: 18, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "bash <(wget -qO- http://website.com/my-script.sh)", + startLine: 20, + endLine: 20, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "pip install -r requirements.txt", + startLine: 26, + endLine: 26, + t: checker.DependencyUseTypePipCommand, + }, + { + snippet: "curl bla | bash", + startLine: 28, + endLine: 28, + t: checker.DependencyUseTypeDownloadThenRun, + }, + { + snippet: "choco install 'some-package'", + startLine: 30, + endLine: 30, + t: checker.DependencyUseTypeChocoCommand, + }, + { + snippet: "choco install 'some-other-package'", + startLine: 31, + endLine: 31, + t: checker.DependencyUseTypeChocoCommand, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + var r checker.PinningDependenciesData + _, err = validateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &r) + if err != nil { + t.Errorf("error during validateShellScriptIsFreeOfInsecureDownloads: %v", err) + } + + for _, expectedDep := range tt.expected { + isExpectedDep := func(dep checker.Dependency) bool { + return dep.Location.Offset == expectedDep.startLine && + dep.Location.EndOffset == expectedDep.endLine && + dep.Location.Path == tt.filename && + dep.Type == expectedDep.t && + dep.Location.Snippet == expectedDep.snippet + } + + if !scut.ValidatePinningDependencies(isExpectedDep, &r) { + t.Errorf("test failed: dependency not present: %+v", tt.expected) + } + } + }) + } +} + +func TestDockerfilePinningWihoutHash(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "Pinned dockerfile as no hash", + filename: "./testdata/Dockerfile-pinned-as-without-hash", + warns: 4, + }, + { + name: "Dockerfile with args", + filename: "./testdata/Dockerfile-args", + warns: 2, + }, + { + name: "Dockerfile with base", + filename: "./testdata/Dockerfile-base", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + var r checker.PinningDependenciesData + _, err = validateDockerfilesPinning(tt.filename, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestDockerfileScriptDownload(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + warns int + err error + name string + filename string + }{ + { + name: "curl | sh", + filename: "./testdata/Dockerfile-curl-sh", + warns: 4, + }, + { + name: "empty file", + filename: "./testdata/Dockerfile-empty", + }, + { + name: "invalid file sh", + filename: "../testdata/script.sh", + }, + { + name: "comments only", + filename: "./testdata/Dockerfile-comments", + }, + { + name: "wget | /bin/sh", + filename: "./testdata/Dockerfile-wget-bin-sh", + warns: 3, + }, + { + name: "wget no exec", + filename: "./testdata/Dockerfile-script-ok", + }, + { + name: "curl file sh", + filename: "./testdata/Dockerfile-curl-file-sh", + warns: 12, + }, + { + name: "proc substitution", + filename: "./testdata/Dockerfile-proc-subs", + warns: 6, + }, + { + name: "wget file", + filename: "./testdata/Dockerfile-wget-file", + warns: 10, + }, + { + name: "gsutil file", + filename: "./testdata/Dockerfile-gsutil-file", + warns: 17, + }, + { + name: "aws file", + filename: "./testdata/Dockerfile-aws-file", + warns: 15, + }, + { + name: "pkg managers", + filename: "./testdata/Dockerfile-pkg-managers", + warns: 39, + }, + { + name: "download with some python", + filename: "./testdata/Dockerfile-some-python", + warns: 1, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + if tt.filename == "" { + content = make([]byte, 0) + } else { + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + } + + var r checker.PinningDependenciesData + _, err = validateDockerfileInsecureDownloads(tt.filename, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestDockerfileScriptDownloadInfo(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + warns int + err error + }{ + { + name: "curl | sh", + filename: "./testdata/Dockerfile-no-curl-sh", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + var r checker.PinningDependenciesData + _, err = validateDockerfileInsecureDownloads(tt.filename, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestShellScriptDownload(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + warns int + debugs int + err error + }{ + { + name: "sh script", + filename: "../testdata/script-sh", + warns: 7, + }, + { + name: "empty file", + filename: "./testdata/script-empty.sh", + }, + { + name: "comments", + filename: "./testdata/script-comments.sh", + }, + { + name: "bash script", + filename: "./testdata/script-bash", + warns: 7, + }, + { + name: "sh script 2", + filename: "../testdata/script.sh", + warns: 7, + }, + { + name: "pkg managers", + filename: "./testdata/script-pkg-managers", + warns: 36, + }, + { + name: "invalid shell script", + filename: "./testdata/script-invalid.sh", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + if tt.filename == "" { + content = make([]byte, 0) + } else { + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + } + + var r checker.PinningDependenciesData + _, err = validateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &r) + + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + // Note: this works because all our examples + // either have warns or debugs. + ws := (tt.warns == len(r.Dependencies)) && (tt.debugs == 0) + ds := (tt.debugs == len(r.Dependencies)) && (tt.warns == 0) + if !ws && !ds { + t.Errorf("expected %v or %v. Got %v", tt.warns, tt.debugs, len(r.Dependencies)) + } + }) + } +} + +func TestShellScriptDownloadPinned(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + warns int + err error + }{ + { + name: "sh script", + filename: "./testdata/script-comments.sh", + }, + { + name: "script free of download", + filename: "./testdata/script-free-from-download.sh", + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + var r checker.PinningDependenciesData + _, err = validateShellScriptIsFreeOfInsecureDownloads(tt.filename, content, &r) + + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestGitHubWorflowRunDownload(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + filename string + warns int + err error + }{ + { + name: "workflow curl default", + filename: "./testdata/.github/workflows/github-workflow-curl-default.yaml", + warns: 1, + }, + { + name: "workflow curl no default", + filename: "./testdata/.github/workflows/github-workflow-curl-no-default.yaml", + warns: 1, + }, + { + name: "wget across steps", + filename: "./testdata/.github/workflows/github-workflow-wget-across-steps.yaml", + warns: 2, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var content []byte + var err error + if tt.filename == "" { + content = make([]byte, 0) + } else { + content, err = os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + } + p := strings.Replace(tt.filename, "./testdata/", "", 1) + + var r checker.PinningDependenciesData + + _, err = validateGitHubWorkflowIsFreeOfInsecureDownloads(p, content, &r) + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if tt.warns != len(r.Dependencies) { + t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + } + }) + } +} + +func TestGitHubWorkflowUsesLineNumber(t *testing.T) { + t.Parallel() + tests := []struct { + name string + filename string + expected []struct { + dependency string + startLine uint + endLine uint + } + }{ + { + name: "unpinned dependency in uses", + filename: "../testdata/.github/workflows/github-workflow-permissions-run-codeql-write.yaml", + expected: []struct { + dependency string + startLine uint + endLine uint + }{ + { + dependency: "github/codeql-action/analyze@v1", + startLine: 25, + endLine: 25, + }, + }, + }, + { + name: "multiple unpinned dependency in uses", + filename: "./testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml", + expected: []struct { + dependency string + startLine uint + endLine uint + }{ + { + dependency: "github/codeql-action/analyze@v1", + startLine: 22, + endLine: 22, + }, + { + dependency: "docker/build-push-action@1.2.3", + startLine: 24, + endLine: 24, + }, + }, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + p := strings.Replace(tt.filename, "../testdata/", "", 1) + p = strings.Replace(p, "./testdata/", "", 1) + var r checker.PinningDependenciesData + + _, err = validateGitHubActionWorkflow(p, content, &r) + if err != nil { + t.Errorf("validateGitHubActionWorkflow: %v", err) + } + for _, expectedDep := range tt.expected { + isExpectedDep := func(dep checker.Dependency) bool { + return dep.Location.Offset == expectedDep.startLine && + dep.Location.EndOffset == expectedDep.endLine && + dep.Location.Path == p && + dep.Location.Snippet == expectedDep.dependency && + dep.Type == checker.DependencyUseTypeGHAction + } + + if !scut.ValidatePinningDependencies(isExpectedDep, &r) { + t.Errorf("test failed: dependency not present: %+v", tt.expected) + } + } + }) + } +} + +func TestGitHubWorkInsecureDownloadsLineNumber(t *testing.T) { + t.Parallel() + tests := []struct { + name string + filename string + expected []struct { + snippet string + startLine uint + endLine uint + } + }{ + { + name: "downloads", + filename: "./testdata/.github/workflows/github-workflow-download-lines.yaml", + expected: []struct { + snippet string + startLine uint + endLine uint + }{ + { + snippet: "bash /tmp/file", + startLine: 27, + endLine: 27, + }, + { + snippet: "/tmp/file2", + startLine: 29, + endLine: 29, + }, + { + snippet: "curl bla | bash", + startLine: 32, + endLine: 32, + }, + }, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := os.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + + p := strings.Replace(tt.filename, "./testdata/", "", 1) + var r checker.PinningDependenciesData + + _, err = validateGitHubWorkflowIsFreeOfInsecureDownloads(p, content, &r) + if err != nil { + t.Errorf("error during validateGitHubWorkflowIsFreeOfInsecureDownloads: %v", err) + } + + for _, expectedDep := range tt.expected { + isExpectedDep := func(dep checker.Dependency) bool { + return dep.Location.Offset == expectedDep.startLine && + dep.Location.EndOffset == expectedDep.endLine && + dep.Location.Path == p && + dep.Location.Snippet == expectedDep.snippet && + dep.Type == checker.DependencyUseTypeDownloadThenRun + } + + if !scut.ValidatePinningDependencies(isExpectedDep, &r) { + t.Errorf("test failed: dependency not present: %+v", tt.expected) + } + } + }) + } +} diff --git a/checks/shell_download_validate.go b/checks/raw/shell_download_validate.go similarity index 85% rename from checks/shell_download_validate.go rename to checks/raw/shell_download_validate.go index 3a5d8ffccb2..3af817c2d77 100644 --- a/checks/shell_download_validate.go +++ b/checks/raw/shell_download_validate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package checks +package raw import ( "bufio" @@ -292,47 +292,51 @@ func getLine(startLine, endLine uint, node syntax.Node) (uint, uint) { startLine + node.Pos().Line() } -func isFetchPipeExecute(startLine, endLine uint, node syntax.Node, cmd, pathfn string, - dl checker.DetailLogger, -) bool { +func collectFetchPipeExecute(startLine, endLine uint, node syntax.Node, cmd, pathfn string, + r *checker.PinningDependenciesData, +) { // BinaryCmd {Op=|, X=CallExpr{Args={curl, -s, url}}, Y=CallExpr{Args={bash,}}}. bc, ok := node.(*syntax.BinaryCmd) if !ok { - return false + return } // Look for the pipe operator. if !strings.EqualFold(bc.Op.String(), "|") { - return false + return } leftStmt, ok := extractCommand(bc.X.Cmd) if !ok { - return false + return } rightStmt, ok := extractCommand(bc.Y.Cmd) if !ok { - return false + return } if !isDownloadUtility(leftStmt) { - return false + return } if !isInterpreter(rightStmt) { - return false + return } startLine, endLine = getLine(startLine, endLine, node) - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "insecure (not pinned by hash) download detected", - }) - return true + + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeDownloadThenRun, + }, + ) } func getRedirectFile(red []*syntax.Redirect) (string, bool) { @@ -356,36 +360,36 @@ func getRedirectFile(red []*syntax.Redirect) (string, bool) { return "", false } -func isExecuteFiles(startLine, endLine uint, node syntax.Node, cmd, pathfn string, files map[string]bool, - dl checker.DetailLogger, -) bool { +func collectExecuteFiles(startLine, endLine uint, node syntax.Node, cmd, pathfn string, files map[string]bool, + r *checker.PinningDependenciesData, +) { ce, ok := node.(*syntax.CallExpr) if !ok { - return false + return } c, ok := extractCommand(ce) if !ok { - return false + return } - ok = false startLine, endLine = getLine(startLine, endLine, node) for fn := range files { if isInterpreterWithFile(c, fn) || isExecuteFile(c, fn) { - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "insecure (not pinned by hash) download-then-run", - }) - ok = true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeDownloadThenRun, + }, + ) } } - - return ok } // Npm install docs are here. @@ -607,75 +611,93 @@ func isChocoUnpinnedDownload(cmd []string) bool { return true } -func isUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.Node, - cmd, pathfn string, dl checker.DetailLogger, -) bool { +func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.Node, + cmd, pathfn string, r *checker.PinningDependenciesData, +) { ce, ok := node.(*syntax.CallExpr) if !ok { - return false + return } c, ok := extractCommand(ce) if !ok { - return false + return } startLine, endLine = getLine(startLine, endLine, node) // Go get/install. if isGoUnpinnedDownload(c) { - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "go installation not pinned by hash", - }) - return true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeGoCommand, + }, + ) + + return } // Pip install. if isPipUnpinnedDownload(c) { - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "pip installation not pinned by hash", - }) - return true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypePipCommand, + }, + ) + + return } // Npm install. if isNpmUnpinnedDownload(c) { - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "npm installation not pinned by hash", - }) - return true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeNpmCommand, + }, + ) + + return } // Choco install. if isChocoUnpinnedDownload(c) { - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "choco installation not pinned by hash", - }) - return true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeChocoCommand, + }, + ) + + return } // TODO(laurent): add other package managers. - - return false } func recordFetchFileFromNode(node syntax.Node) (pathfn string, ok bool, err error) { @@ -701,69 +723,72 @@ func recordFetchFileFromNode(node syntax.Node) (pathfn string, ok bool, err erro return fn, true, nil } -func isFetchProcSubsExecute(startLine, endLine uint, node syntax.Node, cmd, pathfn string, - dl checker.DetailLogger, -) bool { +func collectFetchProcSubsExecute(startLine, endLine uint, node syntax.Node, cmd, pathfn string, + r *checker.PinningDependenciesData, +) { ce, ok := node.(*syntax.CallExpr) if !ok { - return false + return } c, ok := extractCommand(ce) if !ok { - return false + return } if !isInterpreter(c) { - return false + return } // Now parse the process substitution part. // Example: `bash <(wget -qO- http://website.com/my-script.sh)`. l := 2 if len(ce.Args) < l { - return false + return } parts := ce.Args[1].Parts if len(parts) != 1 { - return false + return } part := parts[0] p, ok := part.(*syntax.ProcSubst) if !ok { - return false + return } if !strings.EqualFold(p.Op.String(), "<(") { - return false + return } if len(p.Stmts) == 0 { - return false + return } c, ok = extractCommand(p.Stmts[0].Cmd) if !ok { - return false + return } if !isDownloadUtility(c) { - return false + return } startLine, endLine = getLine(startLine, endLine, node) - dl.Warn(&checker.LogMessage{ - Path: pathfn, - Type: checker.FileTypeSource, - Offset: startLine, - EndOffset: endLine, - Snippet: cmd, - Text: "insecure (not pinned by hash) download-then-run", - }) - return true + r.Dependencies = append(r.Dependencies, + checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: checker.FileTypeSource, + Offset: startLine, + EndOffset: endLine, + Snippet: cmd, + }, + Type: checker.DependencyUseTypeDownloadThenRun, + }, + ) } func isCommand(cmd []string, b string) bool { @@ -840,19 +865,18 @@ func nodeToString(p *syntax.Printer, node syntax.Node) (string, error) { } func validateShellFileAndRecord(pathfn string, startLine, endLine uint, content []byte, files map[string]bool, - dl checker.DetailLogger, -) (bool, error) { + r *checker.PinningDependenciesData, +) error { in := strings.NewReader(string(content)) f, err := syntax.NewParser().Parse(in, pathfn) 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 - return false, sce.WithMessage(sce.ErrorShellParsing, err.Error()) + return sce.WithMessage(sce.ErrorShellParsing, err.Error()) } printer := syntax.NewPrinter() - validated := true syntax.Walk(f, func(node syntax.Node) bool { cmdStr, e := nodeToString(printer, node) @@ -869,9 +893,8 @@ func validateShellFileAndRecord(pathfn string, startLine, endLine uint, content // nolinter if ok && isShellInterpreterOrCommand([]string{i}) { start, end := getLine(startLine, endLine, node) - ok, e := validateShellFileAndRecord(pathfn, start, end, - []byte(c), files, dl) - validated = ok + e := validateShellFileAndRecord(pathfn, start, end, + []byte(c), files, r) if e != nil { err = e return true @@ -879,25 +902,18 @@ func validateShellFileAndRecord(pathfn string, startLine, endLine uint, content } // `curl | bash` (supports `sudo`). - if isFetchPipeExecute(startLine, endLine, node, cmdStr, pathfn, dl) { - validated = false - } + collectFetchPipeExecute(startLine, endLine, node, cmdStr, pathfn, r) // Check if we're calling a file we previously downloaded. // Includes `curl > /tmp/file [&&|;] [bash] /tmp/file` - if isExecuteFiles(startLine, endLine, node, cmdStr, pathfn, files, dl) { - validated = false - } + collectExecuteFiles(startLine, endLine, node, cmdStr, pathfn, files, r) // `bash <(wget -qO- http://website.com/my-script.sh)`. (supports `sudo`). - if isFetchProcSubsExecute(startLine, endLine, node, cmdStr, pathfn, dl) { - validated = false - } + collectFetchProcSubsExecute(startLine, endLine, node, cmdStr, pathfn, r) // Package manager's unpinned installs. - if isUnpinnedPakageManagerDownload(startLine, endLine, node, cmdStr, pathfn, dl) { - validated = false - } + collectUnpinnedPakageManagerDownload(startLine, endLine, node, cmdStr, pathfn, r) + // TODO(laurent): add check for cat file | bash. // TODO(laurent): detect downloads of zip/tar files containing scripts. // TODO(laurent): detect command being an env variable. @@ -916,7 +932,7 @@ func validateShellFileAndRecord(pathfn string, startLine, endLine uint, content return true }) - return validated, err + return err } // The functions below are the only ones that should be called by other files. @@ -991,14 +1007,7 @@ func isMatchingShellScriptFile(pathfn string, content []byte, shellsToMatch []st } func validateShellFile(pathfn string, startLine, endLine uint, - content []byte, taintedFiles map[string]bool, dl checker.DetailLogger, -) (bool, error) { - r, err := validateShellFileAndRecord(pathfn, startLine, endLine, content, taintedFiles, dl) - if err != nil { - // Print this particular error. - dl.Debug(&checker.LogMessage{ - Text: err.Error(), - }) - } - return r, err + content []byte, taintedFiles map[string]bool, r *checker.PinningDependenciesData, +) error { + return validateShellFileAndRecord(pathfn, startLine, endLine, content, taintedFiles, r) } diff --git a/checks/shell_download_validate_test.go b/checks/raw/shell_download_validate_test.go similarity index 93% rename from checks/shell_download_validate_test.go rename to checks/raw/shell_download_validate_test.go index be8d4720eec..bb47a7cc59c 100644 --- a/checks/shell_download_validate_test.go +++ b/checks/raw/shell_download_validate_test.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package checks +package raw import ( "os" "testing" - scut "github.com/ossf/scorecard/v4/utests" + "github.com/ossf/scorecard/v4/checker" ) func TestIsSupportedShellScriptFile(t *testing.T) { @@ -99,8 +99,9 @@ func TestValidateShellFile(t *testing.T) { if err != nil { t.Errorf("cannot read file: %v", err) } - dl := scut.TestDetailLogger{} - _, err = validateShellFile(filename, 0, 0, content, map[string]bool{}, &dl) + + var r checker.PinningDependenciesData + err = validateShellFile(filename, 0, 0, content, map[string]bool{}, &r) if err == nil { t.Errorf("failed to detect shell parsing error: %v", err) } diff --git a/checks/testdata/.github/workflows/github-workflow-comments.yaml b/checks/raw/testdata/.github/workflows/github-workflow-comments.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-comments.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-comments.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-curl-default.yaml b/checks/raw/testdata/.github/workflows/github-workflow-curl-default.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-curl-default.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-curl-default.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-curl-no-default.yaml b/checks/raw/testdata/.github/workflows/github-workflow-curl-no-default.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-curl-no-default.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-curl-no-default.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-download-lines.yaml b/checks/raw/testdata/.github/workflows/github-workflow-download-lines.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-download-lines.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-download-lines.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-empty.yaml b/checks/raw/testdata/.github/workflows/github-workflow-empty.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-empty.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-empty.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-matrix-expression.yaml b/checks/raw/testdata/.github/workflows/github-workflow-matrix-expression.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-matrix-expression.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-matrix-expression.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml b/checks/raw/testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-multiple-unpinned-uses.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-pkg-managers.yaml b/checks/raw/testdata/.github/workflows/github-workflow-pkg-managers.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-pkg-managers.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-pkg-managers.yaml diff --git a/checks/testdata/.github/workflows/github-workflow-wget-across-steps.yaml b/checks/raw/testdata/.github/workflows/github-workflow-wget-across-steps.yaml similarity index 100% rename from checks/testdata/.github/workflows/github-workflow-wget-across-steps.yaml rename to checks/raw/testdata/.github/workflows/github-workflow-wget-across-steps.yaml diff --git a/checks/testdata/.github/workflows/workflow-local-action.yaml b/checks/raw/testdata/.github/workflows/workflow-local-action.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-local-action.yaml rename to checks/raw/testdata/.github/workflows/workflow-local-action.yaml diff --git a/checks/testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml b/checks/raw/testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml rename to checks/raw/testdata/.github/workflows/workflow-mix-github-and-non-github-not-pinned.yaml diff --git a/checks/testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml b/checks/raw/testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml rename to checks/raw/testdata/.github/workflows/workflow-mix-github-and-non-github-pinned.yaml diff --git a/checks/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml b/checks/raw/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml rename to checks/raw/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-github.yaml diff --git a/checks/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml b/checks/raw/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml rename to checks/raw/testdata/.github/workflows/workflow-mix-pinned-and-non-pinned-non-github.yaml diff --git a/checks/testdata/.github/workflows/workflow-non-github-pinned.yaml b/checks/raw/testdata/.github/workflows/workflow-non-github-pinned.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-non-github-pinned.yaml rename to checks/raw/testdata/.github/workflows/workflow-non-github-pinned.yaml diff --git a/checks/testdata/.github/workflows/workflow-not-pinned.yaml b/checks/raw/testdata/.github/workflows/workflow-not-pinned.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-not-pinned.yaml rename to checks/raw/testdata/.github/workflows/workflow-not-pinned.yaml diff --git a/checks/testdata/.github/workflows/workflow-pinned.yaml b/checks/raw/testdata/.github/workflows/workflow-pinned.yaml similarity index 100% rename from checks/testdata/.github/workflows/workflow-pinned.yaml rename to checks/raw/testdata/.github/workflows/workflow-pinned.yaml diff --git a/checks/testdata/Dockerfile-args b/checks/raw/testdata/Dockerfile-args similarity index 100% rename from checks/testdata/Dockerfile-args rename to checks/raw/testdata/Dockerfile-args diff --git a/checks/testdata/Dockerfile-aws-file b/checks/raw/testdata/Dockerfile-aws-file similarity index 100% rename from checks/testdata/Dockerfile-aws-file rename to checks/raw/testdata/Dockerfile-aws-file diff --git a/checks/testdata/Dockerfile-base b/checks/raw/testdata/Dockerfile-base similarity index 100% rename from checks/testdata/Dockerfile-base rename to checks/raw/testdata/Dockerfile-base diff --git a/checks/testdata/Dockerfile-comments b/checks/raw/testdata/Dockerfile-comments similarity index 100% rename from checks/testdata/Dockerfile-comments rename to checks/raw/testdata/Dockerfile-comments diff --git a/checks/testdata/Dockerfile-curl-file-sh b/checks/raw/testdata/Dockerfile-curl-file-sh similarity index 100% rename from checks/testdata/Dockerfile-curl-file-sh rename to checks/raw/testdata/Dockerfile-curl-file-sh diff --git a/checks/testdata/Dockerfile-curl-sh b/checks/raw/testdata/Dockerfile-curl-sh similarity index 100% rename from checks/testdata/Dockerfile-curl-sh rename to checks/raw/testdata/Dockerfile-curl-sh diff --git a/checks/testdata/Dockerfile-download-lines b/checks/raw/testdata/Dockerfile-download-lines similarity index 100% rename from checks/testdata/Dockerfile-download-lines rename to checks/raw/testdata/Dockerfile-download-lines diff --git a/checks/testdata/Dockerfile-download-multi-runs b/checks/raw/testdata/Dockerfile-download-multi-runs similarity index 100% rename from checks/testdata/Dockerfile-download-multi-runs rename to checks/raw/testdata/Dockerfile-download-multi-runs diff --git a/checks/testdata/Dockerfile-empty b/checks/raw/testdata/Dockerfile-empty similarity index 100% rename from checks/testdata/Dockerfile-empty rename to checks/raw/testdata/Dockerfile-empty diff --git a/checks/testdata/Dockerfile-gsutil-file b/checks/raw/testdata/Dockerfile-gsutil-file similarity index 100% rename from checks/testdata/Dockerfile-gsutil-file rename to checks/raw/testdata/Dockerfile-gsutil-file diff --git a/checks/testdata/Dockerfile-invalid b/checks/raw/testdata/Dockerfile-invalid similarity index 100% rename from checks/testdata/Dockerfile-invalid rename to checks/raw/testdata/Dockerfile-invalid diff --git a/checks/testdata/Dockerfile-no-curl-sh b/checks/raw/testdata/Dockerfile-no-curl-sh similarity index 100% rename from checks/testdata/Dockerfile-no-curl-sh rename to checks/raw/testdata/Dockerfile-no-curl-sh diff --git a/checks/testdata/Dockerfile-not-pinned b/checks/raw/testdata/Dockerfile-not-pinned similarity index 100% rename from checks/testdata/Dockerfile-not-pinned rename to checks/raw/testdata/Dockerfile-not-pinned diff --git a/checks/testdata/Dockerfile-not-pinned-as b/checks/raw/testdata/Dockerfile-not-pinned-as similarity index 100% rename from checks/testdata/Dockerfile-not-pinned-as rename to checks/raw/testdata/Dockerfile-not-pinned-as diff --git a/checks/testdata/Dockerfile-pinned b/checks/raw/testdata/Dockerfile-pinned similarity index 100% rename from checks/testdata/Dockerfile-pinned rename to checks/raw/testdata/Dockerfile-pinned diff --git a/checks/testdata/Dockerfile-pinned-as b/checks/raw/testdata/Dockerfile-pinned-as similarity index 100% rename from checks/testdata/Dockerfile-pinned-as rename to checks/raw/testdata/Dockerfile-pinned-as diff --git a/checks/testdata/Dockerfile-pinned-as-without-hash b/checks/raw/testdata/Dockerfile-pinned-as-without-hash similarity index 100% rename from checks/testdata/Dockerfile-pinned-as-without-hash rename to checks/raw/testdata/Dockerfile-pinned-as-without-hash diff --git a/checks/testdata/Dockerfile-pkg-managers b/checks/raw/testdata/Dockerfile-pkg-managers similarity index 100% rename from checks/testdata/Dockerfile-pkg-managers rename to checks/raw/testdata/Dockerfile-pkg-managers diff --git a/checks/testdata/Dockerfile-proc-subs b/checks/raw/testdata/Dockerfile-proc-subs similarity index 100% rename from checks/testdata/Dockerfile-proc-subs rename to checks/raw/testdata/Dockerfile-proc-subs diff --git a/checks/testdata/Dockerfile-script-ok b/checks/raw/testdata/Dockerfile-script-ok similarity index 100% rename from checks/testdata/Dockerfile-script-ok rename to checks/raw/testdata/Dockerfile-script-ok diff --git a/checks/testdata/Dockerfile-some-python b/checks/raw/testdata/Dockerfile-some-python similarity index 100% rename from checks/testdata/Dockerfile-some-python rename to checks/raw/testdata/Dockerfile-some-python diff --git a/checks/testdata/Dockerfile-wget-bin-sh b/checks/raw/testdata/Dockerfile-wget-bin-sh similarity index 100% rename from checks/testdata/Dockerfile-wget-bin-sh rename to checks/raw/testdata/Dockerfile-wget-bin-sh diff --git a/checks/testdata/Dockerfile-wget-file b/checks/raw/testdata/Dockerfile-wget-file similarity index 100% rename from checks/testdata/Dockerfile-wget-file rename to checks/raw/testdata/Dockerfile-wget-file diff --git a/checks/testdata/script-bash b/checks/raw/testdata/script-bash similarity index 100% rename from checks/testdata/script-bash rename to checks/raw/testdata/script-bash diff --git a/checks/testdata/script-comments.sh b/checks/raw/testdata/script-comments.sh similarity index 100% rename from checks/testdata/script-comments.sh rename to checks/raw/testdata/script-comments.sh diff --git a/checks/testdata/script-empty.sh b/checks/raw/testdata/script-empty.sh similarity index 100% rename from checks/testdata/script-empty.sh rename to checks/raw/testdata/script-empty.sh diff --git a/checks/testdata/script-free-from-download.sh b/checks/raw/testdata/script-free-from-download.sh similarity index 100% rename from checks/testdata/script-free-from-download.sh rename to checks/raw/testdata/script-free-from-download.sh diff --git a/checks/testdata/script-invalid.sh b/checks/raw/testdata/script-invalid.sh similarity index 100% rename from checks/testdata/script-invalid.sh rename to checks/raw/testdata/script-invalid.sh diff --git a/checks/testdata/script-pkg-managers b/checks/raw/testdata/script-pkg-managers similarity index 100% rename from checks/testdata/script-pkg-managers rename to checks/raw/testdata/script-pkg-managers diff --git a/checks/testdata/shell-download-lines.sh b/checks/raw/testdata/shell-download-lines.sh similarity index 100% rename from checks/testdata/shell-download-lines.sh rename to checks/raw/testdata/shell-download-lines.sh diff --git a/checks/testdata/shell_file_awk_shebang.sh b/checks/raw/testdata/shell_file_awk_shebang.sh similarity index 100% rename from checks/testdata/shell_file_awk_shebang.sh rename to checks/raw/testdata/shell_file_awk_shebang.sh diff --git a/checks/testdata/shell_file_bash_shebang1.sh b/checks/raw/testdata/shell_file_bash_shebang1.sh similarity index 100% rename from checks/testdata/shell_file_bash_shebang1.sh rename to checks/raw/testdata/shell_file_bash_shebang1.sh diff --git a/checks/testdata/shell_file_bash_shebang2.sh b/checks/raw/testdata/shell_file_bash_shebang2.sh similarity index 100% rename from checks/testdata/shell_file_bash_shebang2.sh rename to checks/raw/testdata/shell_file_bash_shebang2.sh diff --git a/checks/testdata/shell_file_bash_shebang3.sh b/checks/raw/testdata/shell_file_bash_shebang3.sh similarity index 100% rename from checks/testdata/shell_file_bash_shebang3.sh rename to checks/raw/testdata/shell_file_bash_shebang3.sh diff --git a/checks/testdata/shell_file_mksh_shebang.sh b/checks/raw/testdata/shell_file_mksh_shebang.sh similarity index 100% rename from checks/testdata/shell_file_mksh_shebang.sh rename to checks/raw/testdata/shell_file_mksh_shebang.sh diff --git a/checks/testdata/shell_file_no_shebang.sh b/checks/raw/testdata/shell_file_no_shebang.sh similarity index 100% rename from checks/testdata/shell_file_no_shebang.sh rename to checks/raw/testdata/shell_file_no_shebang.sh diff --git a/checks/testdata/shell_file_sh_shebang.sh b/checks/raw/testdata/shell_file_sh_shebang.sh similarity index 100% rename from checks/testdata/shell_file_sh_shebang.sh rename to checks/raw/testdata/shell_file_sh_shebang.sh diff --git a/checks/testdata/shell_file_zsh_shebang.sh b/checks/raw/testdata/shell_file_zsh_shebang.sh similarity index 100% rename from checks/testdata/shell_file_zsh_shebang.sh rename to checks/raw/testdata/shell_file_zsh_shebang.sh diff --git a/checks/remediations.go b/checks/remediations.go index c31e62062f0..5723a9e8b4a 100644 --- a/checks/remediations.go +++ b/checks/remediations.go @@ -72,10 +72,6 @@ func createWorkflowPermissionRemediation(filepath string) *checker.Remediation { return createWorkflowRemediation(filepath, "permissions") } -func createWorkflowPinningRemediation(filepath string) *checker.Remediation { - return createWorkflowRemediation(filepath, "pin") -} - func createWorkflowRemediation(path, t string) *checker.Remediation { p := strings.TrimPrefix(path, ".github/workflows/") if remediationBranch == "" || remediationRepo == "" { diff --git a/e2e/pinned_dependencies_test.go b/e2e/pinned_dependencies_test.go index 62c2352582b..e38f7374c61 100644 --- a/e2e/pinned_dependencies_test.go +++ b/e2e/pinned_dependencies_test.go @@ -50,12 +50,12 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 3, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 2, + NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.PinnedDependencies(&req) + result := checks.PinningDependencies(&req) Expect(scut.ValidateTestReturn(nil, "dependencies check", &expected, &result, &dl)).Should(BeTrue()) Expect(repoClient.Close()).Should(BeNil()) }) @@ -75,12 +75,12 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 3, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 2, + NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.PinnedDependencies(&req) + result := checks.PinningDependencies(&req) Expect(scut.ValidateTestReturn(nil, "dependencies check", &expected, &result, &dl)).Should(BeTrue()) Expect(repoClient.Close()).Should(BeNil()) }) @@ -111,12 +111,12 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 3, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 2, + NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.PinnedDependencies(&req) + result := checks.PinningDependencies(&req) Expect(scut.ValidateTestReturn(nil, "dependencies check", &expected, &result, &dl)).Should(BeTrue()) Expect(x.Close()).Should(BeNil()) }) diff --git a/go.sum b/go.sum index 5be5b820d4e..6af40b10a24 100644 --- a/go.sum +++ b/go.sum @@ -22,7 +22,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= @@ -60,7 +59,6 @@ cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSi cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.4.0/go.mod h1:NjjGEnxCS3CAKYp+vmALu20QzcqasGodQp48WxJGAYc= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= @@ -75,7 +73,6 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.9.0/go.mod h1:G3o6/kJvEMIEAN5urdkaP4be49WQsjNiykBIto9LFtY= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= cloud.google.com/go/pubsub v1.21.1 h1:ghu6wlm6WouITmmuwkxGG+6vNRXDaPdAjqLcRdsw3EQ= cloud.google.com/go/pubsub v1.21.1/go.mod h1:u3XGeMBOBCIQLcxNzy14Svz88ZFS8vI250uDgIAQDSQ= @@ -85,7 +82,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8= cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE= @@ -97,7 +93,6 @@ contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= -contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/exporter/stackdriver v0.13.12 h1:bjBKzIf7/TAkxd7L2utGaLM78bmUWlCval5K9UeElbY= contrib.go.opencensus.io/exporter/stackdriver v0.13.12/go.mod h1:mmxnWlrvrFdpiOHOhxBaVi1rkc0WOqhgfknj4Yg0SeQ= @@ -110,8 +105,6 @@ git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqbl github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/AkihiroSuda/containerd-fuse-overlayfs v1.0.0/go.mod h1:0mMDvQFeLbbn1Wy8P2j3hwFhqBq+FKn8OZPno8WLmp8= github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= -github.com/Azure/azure-amqp-common-go/v3 v3.0.1/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0= -github.com/Azure/azure-amqp-common-go/v3 v3.1.0/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0= github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= github.com/Azure/azure-amqp-common-go/v3 v3.2.2/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= @@ -122,24 +115,18 @@ github.com/Azure/azure-sdk-for-go v19.1.1+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v42.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v49.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= -github.com/Azure/azure-service-bus-go v0.10.7/go.mod h1:o5z/3lDG1iT/T/G7vgIwIqVDTx9Qa2wndf5OdzSzpF8= github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= -github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs= github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM= github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= -github.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs= -github.com/Azure/go-amqp v0.13.1/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs= github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -154,10 +141,6 @@ github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8 github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.7/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs= -github.com/Azure/go-autorest/autorest v0.11.9/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= @@ -167,15 +150,11 @@ github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMl github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= -github.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.6/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.17/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.3/go.mod h1:4bJZhUhcq8LB20TruwHbAQsmUs2Xh+QR7utuJpLXX3A= github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= @@ -192,7 +171,6 @@ github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsI github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= -github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= @@ -204,7 +182,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.19.1/go.mod h1:+yYmuKqcBVkgRePGpUhTA9OEg0XsnFE96eZ6nJ2yCQM= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -287,11 +264,9 @@ github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.43.31 h1:yJZIr8nMV1hXjAvvOLUFqZRJcHV7udPQBfhJqawDzI0= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= @@ -354,9 +329,6 @@ github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6 github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I= -github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug= github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4kz6OXd0PKPlFqf81M= github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= @@ -560,7 +532,6 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -818,7 +789,6 @@ github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bz github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -847,9 +817,6 @@ github.com/google/go-containerregistry v0.9.0/go.mod h1:9eq4BnSufyT1kHNffX+vSXVo github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= -github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= -github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3Uva+po= github.com/google/go-github/v38 v38.1.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4= github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= @@ -858,11 +825,9 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= -github.com/google/go-replayers/grpcreplay v1.0.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-replayers/httpreplay v0.1.2/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= @@ -885,7 +850,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -945,7 +909,6 @@ github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.m github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= @@ -1133,7 +1096,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= @@ -1209,7 +1171,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ= github.com/moby/buildkit v0.10.3 h1:/dGykD8FW+H4p++q5+KqKEo6gAkYKyBQHdawdjVwVAU= @@ -1250,7 +1211,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= -github.com/naveensrinivasan/httpcache v1.2.2/go.mod h1:gpEVVjcTYZA3F1tqYkLqbNvZuf380rhUDaV5OZpyQ88= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/networkplumbing/go-nft v0.2.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -1273,7 +1233,6 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= @@ -1290,7 +1249,6 @@ github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= @@ -1332,8 +1290,6 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/ossf/scorecard v1.2.0 h1:Gf12BN29RZDDSev0suW/DwJyhYWH1XHsIqSmpCChgsE= -github.com/ossf/scorecard v1.2.0/go.mod h1:hc0zwnXi2NHq2aru8A/NoNZ9H+DqZZlYbmOw7jjHi/Q= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -1451,7 +1407,6 @@ github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lz github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= @@ -1522,7 +1477,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= @@ -1682,12 +1636,10 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -gocloud.dev v0.22.0/go.mod h1:z3jKIQ0Es9LALVZFQ3wOvwqAsSLq1R5c/2RdmghDucw= gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4= @@ -1700,7 +1652,6 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1818,7 +1769,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1862,7 +1812,6 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -2099,7 +2048,6 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -2143,18 +2091,12 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -2188,7 +2130,6 @@ google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNV google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= @@ -2202,8 +2143,6 @@ google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= -google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= @@ -2239,7 +2178,6 @@ google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= @@ -2284,15 +2222,11 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2369,7 +2303,6 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= @@ -2410,7 +2343,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= @@ -2551,7 +2483,6 @@ mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index a164168146d..2dcc354c477 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -40,10 +40,10 @@ type jsonScorecardRawResult struct { // TODO: separate each check extraction into its own file. type jsonFile struct { - Snippet *string `json:"snippet,omitempty"` - Path string `json:"path"` - // TODO: change to an uint. - Offset int `json:"offset,omitempty"` + Snippet *string `json:"snippet,omitempty"` + Path string `json:"path"` + Offset uint `json:"offset,omitempty"` + EndOffset uint `json:"endOffset,omitempty"` } type jsonTool struct { @@ -193,6 +193,19 @@ type jsonRun struct { // TODO: add fields, e.g., Result=["success", "failure"] } +type jsonPinningDependenciesData struct { + Dependencies []jsonDependency `json:"dependencies"` +} + +type jsonDependency struct { + // TODO: unique dependency name. + // TODO: Job *WorkflowJob + Location *jsonFile `json:"location"` + Name *string `json:"name"` + PinnedAt *string `json:"pinnedAt"` + Type string `json:"type"` +} + //nolint type jsonRawResults struct { // Workflow results. @@ -220,7 +233,7 @@ type jsonRawResults struct { // structure for it. Contributors jsonContributors `json:"Contributors"` // Commits. - DefaultBranchCommits []jsonDefaultBranchCommit `json:"defaultBrancCommits"` + DefaultBranchCommits []jsonDefaultBranchCommit `json:"defaultBranchCommits"` // Archived status of the repo. ArchivedStatus jsonArchivedStatus `json:"archived"` // Fuzzers. @@ -229,6 +242,8 @@ type jsonRawResults struct { Releases []jsonRelease `json:"releases"` // Packages. Packages []jsonPackage `json:"packages"` + // Dependency pinning. + DependencyPinning jsonPinningDependenciesData `json:"dependencyPinning"` } func (r *jsonScorecardRawResult) addPackagingRawResults(pk *checker.PackagingData) error { @@ -248,7 +263,7 @@ func (r *jsonScorecardRawResult) addPackagingRawResults(pk *checker.PackagingDat jpk.File = &jsonFile{ Path: p.File.Path, - Offset: int(p.File.Offset), + Offset: p.File.Offset, // TODO: Snippet } @@ -265,14 +280,44 @@ func (r *jsonScorecardRawResult) addPackagingRawResults(pk *checker.PackagingDat return nil } +//nolint:unparam +func (r *jsonScorecardRawResult) addDependencyPinningRawResults(pd *checker.PinningDependenciesData) error { + r.Results.DependencyPinning = jsonPinningDependenciesData{} + for i := range pd.Dependencies { + rr := pd.Dependencies[i] + if rr.Location == nil { + continue + } + + v := jsonDependency{ + Location: &jsonFile{ + Path: rr.Location.Path, + Offset: rr.Location.Offset, + EndOffset: rr.Location.EndOffset, + }, + Name: rr.Name, + PinnedAt: rr.PinnedAt, + Type: string(rr.Type), + } + + if rr.Location.Snippet != "" { + v.Location.Snippet = &rr.Location.Snippet + } + + r.Results.DependencyPinning.Dependencies = append(r.Results.DependencyPinning.Dependencies, v) + } + return nil +} + //nolint:unparam func (r *jsonScorecardRawResult) addDangerousWorkflowRawResults(df *checker.DangerousWorkflowData) error { r.Results.Workflows = []jsonWorkflow{} for _, e := range df.Workflows { v := jsonWorkflow{ File: &jsonFile{ - Path: e.File.Path, - Offset: int(e.File.Offset), + Path: e.File.Path, + Offset: e.File.Offset, + EndOffset: e.File.EndOffset, }, Type: string(e.Type), } @@ -616,6 +661,11 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) } + // DependencyPinning. + if err := r.addDependencyPinningRawResults(&raw.PinningDependenciesResults); err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + } + // CII-Best-Practices. if err := r.addOssfBestPracticesRawResults(&raw.CIIBestPracticesResults); err != nil { return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) diff --git a/remediation/remediations.go b/remediation/remediations.go new file mode 100644 index 00000000000..c4a3a5732b3 --- /dev/null +++ b/remediation/remediations.go @@ -0,0 +1,96 @@ +// Copyright 2022 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 remediation + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/clients" +) + +var ( + branch string + repo string + once *sync.Once + setupErr error +) + +var errInvalidArg = errors.New("invalid argument") + +var ( + workflowText = "update your workflow using https://app.stepsecurity.io/secureworkflow/%s/%s/%s?enable=%s" + //nolint + workflowMarkdown = "update your workflow using [https://app.stepsecurity.io](https://app.stepsecurity.io/secureworkflow/%s/%s/%s?enable=%s)" +) + +//nolint:gochecknoinits +func init() { + once = new(sync.Once) +} + +// Setup sets up remediation code. +func Setup(c *checker.CheckRequest) error { + once.Do(func() { + // Get the branch for remediation. + b, err := c.RepoClient.GetDefaultBranch() + if err != nil { + if !errors.Is(err, clients.ErrUnsupportedFeature) { + setupErr = err + } + return + } + if b.Name != nil { + branch = *b.Name + uri := c.Repo.URI() + parts := strings.Split(uri, "/") + if len(parts) != 3 { + setupErr = fmt.Errorf("%w: enpty: %s", errInvalidArg, uri) + return + } + repo = fmt.Sprintf("%s/%s", parts[1], parts[2]) + } + }) + + return setupErr +} + +// CreateWorkflowPermissionRemediation create remediation for workflow permissions. +func CreateWorkflowPermissionRemediation(filepath string) *checker.Remediation { + return createWorkflowRemediation(filepath, "permissions") +} + +// CreateWorkflowPinningRemediation create remediaiton for pinninn GH Actions. +func CreateWorkflowPinningRemediation(filepath string) *checker.Remediation { + return createWorkflowRemediation(filepath, "pin") +} + +func createWorkflowRemediation(path, t string) *checker.Remediation { + p := strings.TrimPrefix(path, ".github/workflows/") + if branch == "" || repo == "" { + return nil + } + + text := fmt.Sprintf(workflowText, repo, p, branch, t) + markdown := fmt.Sprintf(workflowMarkdown, repo, p, branch, t) + + return &checker.Remediation{ + HelpText: text, + HelpMarkdown: markdown, + } +} diff --git a/utests/utlib.go b/utests/utlib.go index eb67e213d8e..55ea91d70b1 100644 --- a/utests/utlib.go +++ b/utests/utlib.go @@ -118,6 +118,18 @@ func ValidateTestReturn( return true } +// ValidatePinningDependencies tests that at least one entry returns true for isExpectedMessage. +func ValidatePinningDependencies(isExpectedDependency func(checker.Dependency) bool, + r *checker.PinningDependenciesData, +) bool { + for _, dep := range r.Dependencies { + if isExpectedDependency(dep) { + return true + } + } + return false +} + // ValidateLogMessage tests that at least one log message returns true for isExpectedMessage. func ValidateLogMessage(isExpectedMessage func(checker.LogMessage, checker.DetailType) bool, dl *TestDetailLogger,