From f991fee32da59b6de1b339cc0376062297463b2f Mon Sep 17 00:00:00 2001 From: Chris McGehee Date: Mon, 13 Dec 2021 20:14:35 -0800 Subject: [PATCH] Adding line numbers for rest of Token-Permessions (and by extension, (#1381) Packaging) --- checks/fileparser/github_workflow.go | 131 +++++++++ checks/packaging.go | 265 +++++++++--------- checks/packaging_test.go | 110 ++++++++ checks/permissions.go | 103 +++---- checks/permissions_test.go | 11 + .../github-workflow-packaging-cargo.yaml | 20 ++ ...thub-workflow-packaging-docker-action.yaml | 23 ++ ...github-workflow-packaging-docker-push.yaml | 20 ++ .../github-workflow-packaging-gem.yaml | 20 ++ .../github-workflow-packaging-go.yaml | 31 ++ .../github-workflow-packaging-gradle.yaml | 21 ++ .../github-workflow-packaging-maven.yaml | 21 ++ .../github-workflow-packaging-npm-github.yaml | 25 ++ .../github-workflow-packaging-npm.yaml | 34 +++ .../github-workflow-packaging-nuget.yaml | 20 ++ .../github-workflow-packaging-pypi.yaml | 26 ++ ...ermissions-run-package-workflow-write.yaml | 1 - ...-permissions-run-write-codeql-comment.yaml | 27 ++ 18 files changed, 723 insertions(+), 186 deletions(-) create mode 100644 checks/packaging_test.go create mode 100644 checks/testdata/github-workflow-packaging-cargo.yaml create mode 100644 checks/testdata/github-workflow-packaging-docker-action.yaml create mode 100644 checks/testdata/github-workflow-packaging-docker-push.yaml create mode 100644 checks/testdata/github-workflow-packaging-gem.yaml create mode 100644 checks/testdata/github-workflow-packaging-go.yaml create mode 100644 checks/testdata/github-workflow-packaging-gradle.yaml create mode 100644 checks/testdata/github-workflow-packaging-maven.yaml create mode 100644 checks/testdata/github-workflow-packaging-npm-github.yaml create mode 100644 checks/testdata/github-workflow-packaging-npm.yaml create mode 100644 checks/testdata/github-workflow-packaging-nuget.yaml create mode 100644 checks/testdata/github-workflow-packaging-pypi.yaml create mode 100644 checks/testdata/github-workflow-permissions-run-write-codeql-comment.yaml diff --git a/checks/fileparser/github_workflow.go b/checks/fileparser/github_workflow.go index 724f2ca2..00304bd3 100644 --- a/checks/fileparser/github_workflow.go +++ b/checks/fileparser/github_workflow.go @@ -22,6 +22,7 @@ import ( "github.com/rhysd/actionlint" + "github.com/ossf/scorecard/v3/checker" sce "github.com/ossf/scorecard/v3/errors" ) @@ -59,6 +60,59 @@ func IsStepExecKind(step *actionlint.Step, kind actionlint.ExecKind) bool { return step.Exec.Kind() == kind } +// GetLineNumber returns the line number for this position. +func GetLineNumber(pos *actionlint.Pos) int { + if pos == nil { + return checker.OffsetDefault + } + return pos.Line +} + +// GetUses returns the 'uses' statement in this step or nil if this step does not have one. +func GetUses(step *actionlint.Step) *actionlint.String { + if step == nil { + return nil + } + if !IsStepExecKind(step, actionlint.ExecKindAction) { + return nil + } + execAction, ok := step.Exec.(*actionlint.ExecAction) + if !ok || execAction == nil { + return nil + } + return execAction.Uses +} + +// getWith returns the 'with' statement in this step or nil if this step does not have one. +func getWith(step *actionlint.Step) map[string]*actionlint.Input { + if step == nil { + return nil + } + if !IsStepExecKind(step, actionlint.ExecKindAction) { + return nil + } + execAction, ok := step.Exec.(*actionlint.ExecAction) + if !ok || execAction == nil { + return nil + } + return execAction.Inputs +} + +// getRun returns the 'run' statement in this step or nil if this step does not have one. +func getRun(step *actionlint.Step) *actionlint.String { + if step == nil { + return nil + } + if !IsStepExecKind(step, actionlint.ExecKindRun) { + return nil + } + execAction, ok := step.Exec.(*actionlint.ExecRun) + if !ok || execAction == nil { + return nil + } + return execAction.Run +} + func getExecRunShell(execRun *actionlint.ExecRun) string { if execRun != nil && execRun.Shell != nil { return execRun.Shell.Value @@ -250,3 +304,80 @@ func IsGitHubOwnedAction(actionName string) bool { c := strings.HasPrefix(actionName, "github/") return a || c } + +// JobMatcher is rule for matching a job. +type JobMatcher struct { + // The text to be logged when a job match is found. + LogText string + // Each step in this field has a matching step in the job. + Steps []*JobMatcherStep +} + +// JobMatcherStep is a single step that needs to be matched. +type JobMatcherStep struct { + // If set, the step's 'Uses' must match this field. Checks that the action name is the same. + Uses string + // If set, the step's 'With' have the keys and values that are in this field. + With map[string]string + // If set, the step's 'Run' must match this field. Does a regex match using this field. + Run string +} + +// Matches returns true if the job matches the job matcher. +func (m *JobMatcher) Matches(job *actionlint.Job) bool { + for _, stepToMatch := range m.Steps { + hasMatch := false + for _, step := range job.Steps { + if stepsMatch(stepToMatch, step) { + hasMatch = true + break + } + } + if !hasMatch { + return false + } + } + return true +} + +// stepsMatch returns true if the fields on 'stepToMatch' match what's in 'step'. +func stepsMatch(stepToMatch *JobMatcherStep, step *actionlint.Step) bool { + // Make sure 'uses' matches if present. + if stepToMatch.Uses != "" { + uses := GetUses(step) + if uses == nil { + return false + } + if !strings.HasPrefix(uses.Value, stepToMatch.Uses+"@") { + return false + } + } + + // Make sure 'with' matches if present. + if len(stepToMatch.With) > 0 { + with := getWith(step) + if with == nil { + return false + } + for keyToMatch, valToMatch := range stepToMatch.With { + input, ok := with[keyToMatch] + if !ok || input == nil || input.Value == nil || input.Value.Value != valToMatch { + return false + } + } + } + + // Make sure 'run' matches if present. + if stepToMatch.Run != "" { + run := getRun(step) + if run == nil { + return false + } + r := regexp.MustCompile(stepToMatch.Run) + if !r.MatchString(run.Value) { + return false + } + } + + return true +} diff --git a/checks/packaging.go b/checks/packaging.go index e258a663..8e50768a 100644 --- a/checks/packaging.go +++ b/checks/packaging.go @@ -17,10 +17,12 @@ package checks import ( "fmt" "path/filepath" - "regexp" "strings" + "github.com/rhysd/actionlint" + "github.com/ossf/scorecard/v3/checker" + "github.com/ossf/scorecard/v3/checks/fileparser" sce "github.com/ossf/scorecard/v3/errors" ) @@ -51,7 +53,12 @@ func Packaging(c *checker.CheckRequest) checker.CheckResult { return checker.CreateRuntimeErrorResult(CheckPackaging, e) } - if !isPackagingWorkflow(string(fc), fp, c.Dlogger) { + workflow, errs := actionlint.Parse(fc) + if len(errs) > 0 && workflow == nil { + e := fileparser.FormatActionlintError(errs) + return checker.CreateRuntimeErrorResult(CheckPackaging, e) + } + if !isPackagingWorkflow(workflow, fp, c.Dlogger) { continue } @@ -87,147 +94,135 @@ func Packaging(c *checker.CheckRequest) checker.CheckResult { } // A packaging workflow. -func isPackagingWorkflow(s, fp string, dl checker.DetailLogger) bool { - // Nodejs packages. - if strings.Contains(s, "actions/setup-node@") { - r1 := regexp.MustCompile(`(?s)registry-url.*https://registry\.npmjs\.org`) - r2 := regexp.MustCompile(`(?s)npm.*publish`) +func isPackagingWorkflow(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { + jobMatchers := []fileparser.JobMatcher{ + { + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "actions/setup-node", + With: map[string]string{"registry-url": "https://registry.npmjs.org"}, + }, + { + Run: "npm.*publish", + }, + }, + LogText: "candidate node publishing workflow using npm", + }, + { + // Java packages with maven. + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "actions/setup-java", + }, + { + Run: "mvn.*deploy", + }, + }, + LogText: "candidate java publishing workflow using maven", + }, + { + // Java packages with gradle. + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "actions/setup-java", + }, + { + Run: "gradle.*publish", + }, + }, + LogText: "candidate java publishing workflow using gradle", + }, + { + // Ruby packages. + Steps: []*fileparser.JobMatcherStep{ + { + Run: "gem.*push", + }, + }, + LogText: "candidate ruby publishing workflow using gem", + }, + { + // NuGet packages. + Steps: []*fileparser.JobMatcherStep{ + { + Run: "nuget.*push", + }, + }, + LogText: "candidate nuget publishing workflow", + }, + { + // Docker packages. + Steps: []*fileparser.JobMatcherStep{ + { + Run: "docker.*push", + }, + }, + LogText: "candidate docker publishing workflow", + }, + { + // Docker packages. + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "docker/build-push-action", + }, + }, + LogText: "candidate docker publishing workflow", + }, + { + // Python packages. + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "actions/setup-python", + }, + { + Uses: "pypa/gh-action-pypi-publish", + }, + }, + LogText: "candidate python publishing workflow using pypi", + }, + { + // Go packages. + Steps: []*fileparser.JobMatcherStep{ + { + Uses: "actions/setup-go", + }, + { + Uses: "goreleaser/goreleaser-action", + }, + }, + LogText: "candidate golang publishing workflow", + }, + { + // Rust packages. https://doc.rust-lang.org/cargo/reference/publishing.html + Steps: []*fileparser.JobMatcherStep{ + { + Run: "cargo.*publish", + }, + }, + LogText: "candidate rust publishing workflow using cargo", + }, + } + + for _, job := range workflow.Jobs { + for _, matcher := range jobMatchers { + if !matcher.Matches(job) { + continue + } - if r1.MatchString(s) && r2.MatchString(s) { dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate node publishing workflow using npm", + Path: fp, + Type: checker.FileTypeSource, + Offset: fileparser.GetLineNumber(job.Pos), + Text: matcher.LogText, }) return true } } - // Java packages. - if strings.Contains(s, "actions/setup-java@") { - // Java packages with maven. - r1 := regexp.MustCompile(`(?s)mvn.*deploy`) - if r1.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate java publishing workflow using maven", - }) - return true - } - - // Java packages with gradle. - r2 := regexp.MustCompile(`(?s)gradle.*publish`) - if r2.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate java publishing workflow using gradle", - }) - return true - } - } - - // Ruby packages. - r := regexp.MustCompile(`(?s)gem.*push`) - if r.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate ruby publishing workflow using gem", - }) - return true - } - - // NuGet packages. - r = regexp.MustCompile(`(?s)nuget.*push`) - if r.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate nuget publishing workflow", - }) - return true - } - - // Docker packages. - if strings.Contains(s, "docker/build-push-action@") { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate docker publishing workflow", - }) - return true - } - - r = regexp.MustCompile(`(?s)docker.*push`) - if r.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate docker publishing workflow", - }) - return true - } - - // Python packages. - if strings.Contains(s, "actions/setup-python@") && strings.Contains(s, "pypa/gh-action-pypi-publish@master") { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate python publishing workflow using pypi", - }) - return true - } - - // Go packages. - if strings.Contains(s, "actions/setup-go") && - strings.Contains(s, "goreleaser/goreleaser-action@") { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate golang publishing workflow", - }) - return true - } - - // Rust packages. - // https://doc.rust-lang.org/cargo/reference/publishing.html. - r = regexp.MustCompile(`(?s)cargo.*publish`) - if r.MatchString(s) { - dl.Info3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, - Text: "candidate rust publishing workflow using cargo", - }) - return true - } - dl.Debug3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // Source file must have line number > 0. - Offset: 1, + Path: fp, + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, Text: "not a publishing workflow", }) return false diff --git a/checks/packaging_test.go b/checks/packaging_test.go new file mode 100644 index 00000000..f3c5f397 --- /dev/null +++ b/checks/packaging_test.go @@ -0,0 +1,110 @@ +// 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 checks + +import ( + "fmt" + "os" + "testing" + + "github.com/rhysd/actionlint" + + scut "github.com/ossf/scorecard/v3/utests" +) + +func TestIsPackagingWorkflow(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + filename string + expected bool + }{ + { + name: "npmjs.org publish", + filename: "./testdata/github-workflow-packaging-npm.yaml", + expected: true, + }, + { + name: "npm github publish", + filename: "./testdata/github-workflow-packaging-npm-github.yaml", + expected: false, // Should this be false? + }, + { + name: "maven publish", + filename: "./testdata/github-workflow-packaging-maven.yaml", + expected: true, + }, + { + name: "gradle publish", + filename: "./testdata/github-workflow-packaging-gradle.yaml", + expected: true, + }, + { + name: "gem publish", + filename: "./testdata/github-workflow-packaging-gem.yaml", + expected: true, + }, + { + name: "nuget publish", + filename: "./testdata/github-workflow-packaging-nuget.yaml", + expected: true, + }, + { + name: "docker action publish", + filename: "./testdata/github-workflow-packaging-docker-action.yaml", + expected: true, + }, + { + name: "docker push publish", + filename: "./testdata/github-workflow-packaging-docker-push.yaml", + expected: true, + }, + { + name: "pypi publish", + filename: "./testdata/github-workflow-packaging-pypi.yaml", + expected: true, + }, + { + name: "go publish", + filename: "./testdata/github-workflow-packaging-go.yaml", + expected: true, + }, + { + name: "cargo publish", + filename: "./testdata/github-workflow-packaging-cargo.yaml", + 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() + content, err := os.ReadFile(tt.filename) + if err != nil { + panic(fmt.Errorf("cannot read file: %w", err)) + } + workflow, errs := actionlint.Parse(content) + if len(errs) > 0 && workflow == nil { + panic(fmt.Errorf("cannot parse file: %w", err)) + } + dl := scut.TestDetailLogger{} + result := isPackagingWorkflow(workflow, tt.filename, &dl) + if result != tt.expected { + t.Errorf("isPackagingWorkflow() = %v, expected %v", result, tt.expected) + } + }) + } +} diff --git a/checks/permissions.go b/checks/permissions.go index f57a5813..e2c2006a 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -64,10 +64,7 @@ func validatePermission(permissionKey string, permissionValue *actionlint.Permis return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error()) } val := permissionValue.Value.Value - lineNumber := checker.OffsetDefault - if permissionValue.Value.Pos != nil { - lineNumber = permissionValue.Value.Pos.Line - } + lineNumber := fileparser.GetLineNumber(permissionValue.Value.Pos) if strings.EqualFold(val, "write") { if isPermissionOfInterest(permissionKey, ignoredPermissions) { dl.Warn3(&checker.LogMessage{ @@ -138,11 +135,7 @@ func validatePermissions(permissions *actionlint.Permissions, permLevel, path st } if allIsSet { val := permissions.All.Value - lineNumber := checker.OffsetDefault - if permissions.All.Pos != nil { - lineNumber = permissions.All.Pos.Line - } - + lineNumber := fileparser.GetLineNumber(permissions.All.Pos) if !strings.EqualFold(val, "read-all") && val != "" { dl.Warn3(&checker.LogMessage{ Path: path, @@ -195,14 +188,10 @@ func validateRunLevelPermissions(workflow *actionlint.Workflow, path string, // For most workflows, no write permissions are needed, // so only top-level read-only permissions need to be declared. if job.Permissions == nil { - lineNumber := checker.OffsetDefault - if job.Pos != nil { - lineNumber = job.Pos.Line - } dl.Debug3(&checker.LogMessage{ Path: path, Type: checker.FileTypeSource, - Offset: lineNumber, + Offset: fileparser.GetLineNumber(job.Pos), Text: fmt.Sprintf("no %s permission defined", runLevelPermission), }) recordAllPermissionsWrite(pdata.runLevelWritePermissions) @@ -385,7 +374,7 @@ func validateGitHubActionTokenPermissions(path string, content []byte, // 2. Run-level permission definitions, // see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions. - ignoredPermissions := createIgnoredPermissions(string(content), path, dl) + ignoredPermissions := createIgnoredPermissions(workflow, path, dl) if err := validateRunLevelPermissions(workflow, path, dl, pdata, ignoredPermissions); err != nil { return false, err } @@ -397,15 +386,15 @@ func validateGitHubActionTokenPermissions(path string, content []byte, return true, nil } -func createIgnoredPermissions(s, fp string, dl checker.DetailLogger) map[string]bool { +func createIgnoredPermissions(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) map[string]bool { ignoredPermissions := make(map[string]bool) - if requiresPackagesPermissions(s, fp, dl) { + if requiresPackagesPermissions(workflow, fp, dl) { ignoredPermissions["packages"] = true } - if requiresContentsPermissions(s, fp, dl) { + if requiresContentsPermissions(workflow, fp, dl) { ignoredPermissions["contents"] = true } - if isSARIFUploadWorkflow(s, fp, dl) { + if isSARIFUploadWorkflow(workflow, fp, dl) { ignoredPermissions["security-events"] = true } @@ -413,12 +402,12 @@ func createIgnoredPermissions(s, fp string, dl checker.DetailLogger) map[string] } // Scanning tool run externally and SARIF file uploaded. -func isSARIFUploadWorkflow(s, fp string, dl checker.DetailLogger) bool { +func isSARIFUploadWorkflow(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { //nolint // CodeQl analysis workflow automatically sends sarif file to GitHub. // https://docs.github.com/en/code-security/secure-coding/integrating-with-code-scanning/uploading-a-sarif-file-to-github#about-sarif-file-uploads-for-code-scanning. // `The CodeQL action uploads the SARIF file automatically when it completes analysis`. - if isCodeQlAnalysisWorkflow(s, fp, dl) { + if isCodeQlAnalysisWorkflow(workflow, fp, dl) { return true } @@ -426,7 +415,7 @@ func isSARIFUploadWorkflow(s, fp string, dl checker.DetailLogger) bool { // Third-party scanning tools use the SARIF-upload action from code-ql. // https://docs.github.com/en/code-security/secure-coding/integrating-with-code-scanning/uploading-a-sarif-file-to-github#uploading-a-code-scanning-analysis-with-github-actions // We only support CodeQl today. - if isSARIFUploadAction(s, fp, dl) { + if isSARIFUploadAction(workflow, fp, dl) { return true } @@ -438,22 +427,29 @@ func isSARIFUploadWorkflow(s, fp string, dl checker.DetailLogger) bool { } // CodeQl run externally and SARIF file uploaded. -func isSARIFUploadAction(s, fp string, dl checker.DetailLogger) bool { - if strings.Contains(s, "github/codeql-action/upload-sarif@") { - dl.Debug3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // TODO: set line. - Offset: 1, - Text: "codeql SARIF upload workflow detected", - // TODO: set Snippet. - }) - return true +func isSARIFUploadAction(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { + for _, job := range workflow.Jobs { + for _, step := range job.Steps { + uses := fileparser.GetUses(step) + if uses == nil { + continue + } + if strings.HasPrefix(uses.Value, "github/codeql-action/upload-sarif@") { + dl.Debug3(&checker.LogMessage{ + Path: fp, + Type: checker.FileTypeSource, + Offset: fileparser.GetLineNumber(uses.Pos), + Text: "codeql SARIF upload workflow detected", + // TODO: set Snippet. + }) + return true + } + } } dl.Debug3(&checker.LogMessage{ Path: fp, Type: checker.FileTypeSource, - Offset: 1, + Offset: checker.OffsetDefault, Text: "not a codeql upload SARIF workflow", }) return false @@ -463,22 +459,29 @@ func isSARIFUploadAction(s, fp string, dl checker.DetailLogger) bool { // CodeQl run within GitHub worklow automatically bubbled up to // security events, see // https://docs.github.com/en/code-security/secure-coding/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning. -func isCodeQlAnalysisWorkflow(s, fp string, dl checker.DetailLogger) bool { - if strings.Contains(s, "github/codeql-action/analyze@") { - dl.Debug3(&checker.LogMessage{ - Path: fp, - Type: checker.FileTypeSource, - // TODO: set line. - Offset: 1, - Text: "codeql workflow detected", - // TODO: set Snippet. - }) - return true +func isCodeQlAnalysisWorkflow(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { + for _, job := range workflow.Jobs { + for _, step := range job.Steps { + uses := fileparser.GetUses(step) + if uses == nil { + continue + } + if strings.HasPrefix(uses.Value, "github/codeql-action/analyze@") { + dl.Debug3(&checker.LogMessage{ + Path: fp, + Type: checker.FileTypeSource, + Offset: fileparser.GetLineNumber(uses.Pos), + Text: "codeql workflow detected", + // TODO: set Snippet. + }) + return true + } + } } dl.Debug3(&checker.LogMessage{ Path: fp, Type: checker.FileTypeSource, - Offset: 1, + Offset: checker.OffsetDefault, Text: "not a codeql workflow", }) return false @@ -486,19 +489,19 @@ func isCodeQlAnalysisWorkflow(s, fp string, dl checker.DetailLogger) bool { // A packaging workflow using GitHub's supported packages: // https://docs.github.com/en/packages. -func requiresPackagesPermissions(s, fp string, dl checker.DetailLogger) bool { +func requiresPackagesPermissions(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { // TODO: add support for GitHub registries. // Example: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry. // This feature requires parsing actions properly. // For now, we just re-use the Packaging check to verify that the // workflow is a packaging workflow. - return isPackagingWorkflow(s, fp, dl) + return isPackagingWorkflow(workflow, fp, dl) } // Note: this needs to be improved. // Currently we don't differentiate between publishing on GitHub vs // pubishing on registries. In terms of risk, both are similar, as // an attacker would gain the ability to push a package. -func requiresContentsPermissions(s, fp string, dl checker.DetailLogger) bool { - return requiresPackagesPermissions(s, fp, dl) +func requiresContentsPermissions(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool { + return requiresPackagesPermissions(workflow, fp, dl) } diff --git a/checks/permissions_test.go b/checks/permissions_test.go index 20124328..8171a1bc 100644 --- a/checks/permissions_test.go +++ b/checks/permissions_test.go @@ -274,6 +274,17 @@ func TestGithubTokenPermissions(t *testing.T) { NumberOfDebug: 4, }, }, + { + name: "security-events write, codeql comment", + filename: "./testdata/github-workflow-permissions-run-write-codeql-comment.yaml", + expected: scut.TestReturn{ + Error: nil, + Score: checker.MaxResultScore - 1, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 4, + }, + }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below diff --git a/checks/testdata/github-workflow-packaging-cargo.yaml b/checks/testdata/github-workflow-packaging-cargo.yaml new file mode 100644 index 00000000..9612f831 --- /dev/null +++ b/checks/testdata/github-workflow-packaging-cargo.yaml @@ -0,0 +1,20 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: cargo publish diff --git a/checks/testdata/github-workflow-packaging-docker-action.yaml b/checks/testdata/github-workflow-packaging-docker-action.yaml new file mode 100644 index 00000000..ae45d62f --- /dev/null +++ b/checks/testdata/github-workflow-packaging-docker-action.yaml @@ -0,0 +1,23 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker/build-push-action@v2 + with: + push: true + tags: user/app:latest diff --git a/checks/testdata/github-workflow-packaging-docker-push.yaml b/checks/testdata/github-workflow-packaging-docker-push.yaml new file mode 100644 index 00000000..ca08ea6c --- /dev/null +++ b/checks/testdata/github-workflow-packaging-docker-push.yaml @@ -0,0 +1,20 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: docker push myapp/myimage:latest diff --git a/checks/testdata/github-workflow-packaging-gem.yaml b/checks/testdata/github-workflow-packaging-gem.yaml new file mode 100644 index 00000000..416ad343 --- /dev/null +++ b/checks/testdata/github-workflow-packaging-gem.yaml @@ -0,0 +1,20 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: gem push *.gem diff --git a/checks/testdata/github-workflow-packaging-go.yaml b/checks/testdata/github-workflow-packaging-go.yaml new file mode 100644 index 00000000..29c17d06 --- /dev/null +++ b/checks/testdata/github-workflow-packaging-go.yaml @@ -0,0 +1,31 @@ +# Copyright 2021 Security Scorecard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-go@v2 + with: + go-version: 1.17 + - uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/checks/testdata/github-workflow-packaging-gradle.yaml b/checks/testdata/github-workflow-packaging-gradle.yaml new file mode 100644 index 00000000..31817a80 --- /dev/null +++ b/checks/testdata/github-workflow-packaging-gradle.yaml @@ -0,0 +1,21 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + - run: gradle publish diff --git a/checks/testdata/github-workflow-packaging-maven.yaml b/checks/testdata/github-workflow-packaging-maven.yaml new file mode 100644 index 00000000..c34818ba --- /dev/null +++ b/checks/testdata/github-workflow-packaging-maven.yaml @@ -0,0 +1,21 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + - run: mvn deploy diff --git a/checks/testdata/github-workflow-packaging-npm-github.yaml b/checks/testdata/github-workflow-packaging-npm-github.yaml new file mode 100644 index 00000000..793a75af --- /dev/null +++ b/checks/testdata/github-workflow-packaging-npm-github.yaml @@ -0,0 +1,25 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + registry-url: 'https://npm.pkg.github.com' + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/checks/testdata/github-workflow-packaging-npm.yaml b/checks/testdata/github-workflow-packaging-npm.yaml new file mode 100644 index 00000000..c9d0ebec --- /dev/null +++ b/checks/testdata/github-workflow-packaging-npm.yaml @@ -0,0 +1,34 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '14.x' + registry-url: 'https://registry.npmjs.org' + - run: npm install + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: actions/setup-node@v2 + with: + registry-url: 'https://npm.pkg.github.com' + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/checks/testdata/github-workflow-packaging-nuget.yaml b/checks/testdata/github-workflow-packaging-nuget.yaml new file mode 100644 index 00000000..8f582d04 --- /dev/null +++ b/checks/testdata/github-workflow-packaging-nuget.yaml @@ -0,0 +1,20 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: nuget push **\*.nupkg -Source 'https://nuget.pkg.github.com/MyOrg/index.json' -ApiKey ${{secrets.NUGET_TOKEN}} diff --git a/checks/testdata/github-workflow-packaging-pypi.yaml b/checks/testdata/github-workflow-packaging-pypi.yaml new file mode 100644 index 00000000..273a911a --- /dev/null +++ b/checks/testdata/github-workflow-packaging-pypi.yaml @@ -0,0 +1,26 @@ +# 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. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.9 + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/checks/testdata/github-workflow-permissions-run-package-workflow-write.yaml b/checks/testdata/github-workflow-permissions-run-package-workflow-write.yaml index 6267fda7..86020162 100644 --- a/checks/testdata/github-workflow-permissions-run-package-workflow-write.yaml +++ b/checks/testdata/github-workflow-permissions-run-package-workflow-write.yaml @@ -22,5 +22,4 @@ jobs: packages: write steps: - name: some name - run: echo "write-and-read workflow" uses: docker/build-push-action@1.2.3 \ No newline at end of file diff --git a/checks/testdata/github-workflow-permissions-run-write-codeql-comment.yaml b/checks/testdata/github-workflow-permissions-run-write-codeql-comment.yaml new file mode 100644 index 00000000..44eb3e1b --- /dev/null +++ b/checks/testdata/github-workflow-permissions-run-write-codeql-comment.yaml @@ -0,0 +1,27 @@ +# 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. +name: write-and-read workflow +on: [push] +permissions: read-all + +jobs: + Explore-GitHub-Actions: + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Perform CodeQL Analysis + uses: github/some-action/analyze@v1 + run: echo "write-and-read workflow" + # Some comment about github/codeql-action/analyze@v1