From b4ec86df5f6b2b61b266fe64ff897f6841bb06f3 Mon Sep 17 00:00:00 2001 From: Diogo Teles Sant'Anna Date: Sat, 29 Jun 2024 04:54:50 +0000 Subject: [PATCH 01/31] Merge pull request #1 from joycebrum/feature/setup-environment-for-dw-fix create environment for patch on DW script injections Signed-off-by: Diogo Teles Sant'Anna Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/patch/impl.go | 0 probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go | 0 .../hasDangerousWorkflowScriptInjection/patch/parse_workflow.go | 0 .../patch/parse_workflow_test.go | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/impl.go create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go new file mode 100644 index 00000000000..e69de29bb2d diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go new file mode 100644 index 00000000000..e69de29bb2d diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go new file mode 100644 index 00000000000..e69de29bb2d diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go new file mode 100644 index 00000000000..e69de29bb2d From 5ee165c356470f631b317ac684ffae180f1360b3 Mon Sep 17 00:00:00 2001 From: Diogo Teles Sant'Anna Date: Sat, 29 Jun 2024 04:49:54 +0000 Subject: [PATCH 02/31] Merge pull request #3 from joycebrum/feat/connect-patch-generator-with-remediation-output Include the generated patch in the output Signed-off-by: Joyce Brum Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 3 + .../patch/impl.go | 55 +++++++++++++++++++ .../patch/impl_test.go | 15 +++++ .../patch/parse_workflow.go | 15 +++++ .../patch/parse_workflow_test.go | 15 +++++ 5 files changed, 103 insertions(+) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 1ec39da97dc..e16b4d63164 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -23,6 +23,7 @@ import ( "github.com/ossf/scorecard/v5/finding" "github.com/ossf/scorecard/v5/internal/checknames" "github.com/ossf/scorecard/v5/internal/probes" + "github.com/ossf/scorecard/v5/probes/hasDangerousWorkflowScriptInjection/patch" "github.com/ossf/scorecard/v5/probes/internal/utils/uerror" ) @@ -68,6 +69,8 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { LineStart: &e.File.Offset, Snippet: &e.File.Snippet, }) + patch := patch.GeneratePatch(e.File) + f.WithPatch(&patch) findings = append(findings, *f) } } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index e69de29bb2d..cc7e8be17a4 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -0,0 +1,55 @@ +// Copyright 2024 OpenSSF 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 patch + +import ( + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/ossf/scorecard/v5/checker" +) + +func parseDiff(diff string) string { + i := strings.Index(diff, "\"\"\"\n") + if i == -1 { + return diff + } + //remove everything before """\n + diff = diff[i+4:] + i = strings.LastIndex(diff, "\"\"\"") + if i == -1 { + return diff + } + //remove everything after \n \t""" + return diff[:i] +} + +// TODO: Receive the dangerous workflow as parameter +func GeneratePatch(f checker.File) string { + // TODO: Implement + // example: + // type scriptInjection + // path {.github/workflows/active-elastic-job~active-elastic-job~build.yml github.head_ref 91 0 0 1} + // snippet github.head_ref + src := `asasas +hello """ola""" + message=$(echo "${{ github.event.head_commit.message }}" | tail -n +3) +adios` + dst := `asasas +hello """ola""" + message=$(echo $COMMIT | tail -n +3) +adios` + return parseDiff(cmp.Diff(src, dst)) +} diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index e69de29bb2d..ad10ca811b0 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -0,0 +1,15 @@ +// Copyright 2024 OpenSSF 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 patch diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go index e69de29bb2d..ad10ca811b0 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go @@ -0,0 +1,15 @@ +// Copyright 2024 OpenSSF 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 patch diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go index e69de29bb2d..ad10ca811b0 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go @@ -0,0 +1,15 @@ +// Copyright 2024 OpenSSF 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 patch From bcb159e214e1ebbf3ed2ecfdc0884e40c5aa020e Mon Sep 17 00:00:00 2001 From: Diogo Teles Sant'Anna Date: Sat, 29 Jun 2024 04:27:26 +0000 Subject: [PATCH 03/31] Merge pull request #2 from joycebrum/test/initial-tests-for-dw-fix Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 4 +- .../patch/impl.go | 9 +- .../patch/impl_test.go | 136 ++++++++++++++++++ .../patch/testdata/allKindsOfUserInput.yaml | 42 ++++++ .../testdata/allKindsOfUserInput_fixed.yaml | 67 +++++++++ .../testdata/crazyButValidIndentation.yaml | 36 +++++ .../crazyButValidIndentation_fixed.yaml | 39 +++++ .../fourSpacesIndendationExistentEnvVar.yaml | 27 ++++ ...SpacesIndendationExistentEnvVar_fixed.yaml | 28 ++++ .../testdata/ignorePatternInsideComments.yaml | 24 ++++ .../patch/testdata/newlineOnEOF.yaml | 26 ++++ .../patch/testdata/newlineOnEOF_fixed.yaml | 29 ++++ .../testdata/noLineBreaksBetweenBlocks.yaml | 24 ++++ .../noLineBreaksBetweenBlocks_fixed.yaml | 26 ++++ .../patch/testdata/realExample1.yaml | 58 ++++++++ .../patch/testdata/realExample1_fixed.yaml | 61 ++++++++ .../patch/testdata/realExample2.yaml | 99 +++++++++++++ .../patch/testdata/realExample2_fixed.yaml | 102 +++++++++++++ .../patch/testdata/realExample3.yaml | 44 ++++++ .../patch/testdata/realExample3_fixed.yaml | 47 ++++++ .../testdata/reuseEnvVarSmallerScope.yaml | 53 +++++++ .../reuseEnvVarSmallerScope_fixed.yaml | 53 +++++++ .../testdata/reuseEnvVarWithDiffName.yaml | 31 ++++ .../reuseEnvVarWithDiffName_fixed.yaml | 31 ++++ .../testdata/reuseWorkflowLevelEnvVars.yaml | 70 +++++++++ .../reuseWorkflowLevelEnvVars_fixed.yaml | 72 ++++++++++ .../testdata/twoInjectionsDifferentJobs.yaml | 28 ++++ .../twoInjectionsDifferentJobs_fixed.yaml | 32 +++++ .../patch/testdata/twoInjectionsSameJob.yaml | 26 ++++ .../testdata/twoInjectionsSameJob_fixed.yaml | 30 ++++ .../patch/testdata/twoInjectionsSameStep.yaml | 25 ++++ .../testdata/twoInjectionsSameStep_fixed.yaml | 29 ++++ .../testdata/userInputAssignedToVariable.yaml | 31 ++++ .../userInputAssignedToVariable_fixed.yaml | 34 +++++ 34 files changed, 1468 insertions(+), 5 deletions(-) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index e16b4d63164..00c60d4d7db 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -69,8 +69,8 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { LineStart: &e.File.Offset, Snippet: &e.File.Snippet, }) - patch := patch.GeneratePatch(e.File) - f.WithPatch(&patch) + findingPatch := patch.GeneratePatch(e.File) + f.WithPatch(&findingPatch) findings = append(findings, *f) } } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index cc7e8be17a4..97e9e981ae6 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -26,17 +26,20 @@ func parseDiff(diff string) string { if i == -1 { return diff } - //remove everything before """\n + // remove everything before """\n diff = diff[i+4:] i = strings.LastIndex(diff, "\"\"\"") if i == -1 { return diff } - //remove everything after \n \t""" + // remove everything after \n \t""" return diff[:i] } -// TODO: Receive the dangerous workflow as parameter +// Placeholder function that should receive the file of a workflow and +// return the end result of the Script Injection patch +// +// TODO: Receive the dangerous workflow as parameter. func GeneratePatch(f checker.File) string { // TODO: Implement // example: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index ad10ca811b0..95b517a5da1 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -13,3 +13,139 @@ // limitations under the License. package patch + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ossf/scorecard/v5/checker" +) + +func Test_GeneratePatch(t *testing.T) { + t.Parallel() + tests := []struct { + name string + inputFilepath string + expectedFilepath string + // err error + }{ + // Extracted from real Angular fix: https://github.com/angular/angular/pull/51026/files + { + name: "Real Example 1", + inputFilepath: "realExample1.yaml", + expectedFilepath: "realExample1_fixed.yaml", + }, + // Inspired on a real fix: https://github.com/googleapis/google-cloud-go/pull/9011/files + { + name: "Real Example 2", + inputFilepath: "realExample2.yaml", + expectedFilepath: "realExample2_fixed.yaml", + }, + // Inspired from a real lit/lit fix: https://github.com/lit/lit/pull/3669/files + { + name: "Real Example 3", + inputFilepath: "realExample3.yaml", + expectedFilepath: "realExample3_fixed.yaml", + }, + { + name: "Test all (or most) types of user input that should be detected", + inputFilepath: "allKindsOfUserInput.yaml", + expectedFilepath: "allKindsOfUserInput_fixed.yaml", + }, + { + name: "User's input is assigned to a variable before used", + inputFilepath: "userInputAssignedToVariable.yaml", + expectedFilepath: "userInputAssignedToVariable_fixed.yaml", + }, + { + name: "Two incidences in different jobs", + inputFilepath: "twoInjectionsDifferentJobs.yaml", + expectedFilepath: "twoInjectionsDifferentJobs_fixed.yaml", + }, + { + name: "Two incidences in same job", + inputFilepath: "twoInjectionsSameJob.yaml", + expectedFilepath: "twoInjectionsSameJob_fixed.yaml", + }, + { + name: "Two incidences in same step", + inputFilepath: "twoInjectionsSameStep.yaml", + expectedFilepath: "twoInjectionsSameStep_fixed.yaml", + }, + { + name: "Reuse existent workflow level env var, if has the same name we'd give", + inputFilepath: "reuseWorkflowLevelEnvVars.yaml", + expectedFilepath: "reuseWorkflowLevelEnvVars_fixed.yaml", + }, + // Test currently failing because we don't look for existent env vars pointing to the same content. + // Once proper behavior is implemented, enable this test + // { + // name: "Reuse existent workflow level env var, if it DOES NOT have the same name we'd give", + // inputFilepath: "reuseEnvVarWithDiffName.yaml", + // expectedFilepath: "reuseEnvVarWithDiffName_fixed.yaml", + // }, + + // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. + // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed + // by the ones in smaller scope. + // Once proper behavior is implemented, enable this test + // { + // name: "Reuse env var already existent on smaller scope, it convers case of same or different names", + // inputFilepath: "reuseEnvVarSmallerScope.yaml", + // expectedFilepath: "reuseEnvVarSmallerScope_fixed.yaml", + // }, + { + name: "4-spaces indentation is kept the same", + inputFilepath: "fourSpacesIndentationExistentEnvVar.yaml", + expectedFilepath: "fourSpacesIndentationExistentEnvVar_fixed.yaml", + }, + { + name: "Crazy but valid indentation is kept the same", + inputFilepath: "crazyButValidIndentation.yaml", + expectedFilepath: "crazyButValidIndentation_fixed.yaml", + }, + { + name: "Newline on EOF is kept", + inputFilepath: "newlineOnEOF.yaml", + expectedFilepath: "newlineOnEOF_fixed.yaml", + }, + // Test currently failing due to lack of style awareness. Currently we always add a blankline after + // the env block. + // Once proper behavior is implemented, enable this test. + // { + // name: "Keep style if file doesnt use blank lines between blocks", + // inputFilepath: "noLineBreaksBetweenBlocks.yaml", + // expectedFilepath: "noLineBreaksBetweenBlocks_fixed.yaml", + // }, + { + name: "Ignore if user input regex is just part of a comment", + inputFilepath: "ignorePatternInsideComments.yaml", + expectedFilepath: "ignorePatternInsideComments.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() + + inputFile := checker.File{ + Path: tt.inputFilepath, + } + + expectedContent, err := os.ReadFile("./testdata/" + tt.expectedFilepath) + if err != nil { + t.Errorf("Couldn't read expected testfile. Error:\n%s", err) + } + + output := GeneratePatch(inputFile) + if diff := cmp.Diff(string(expectedContent[:]), output); diff != "" { + // Uncomment the line bellow when the script is fully implemented and the tests are adapted to + // the official input/output + + // t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml new file mode 100644 index 00000000000..c18f9995c47 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 OpenSSF 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. +on: + issue: + +jobs: + pretty-generic-job: + steps: + - name: everything everywhere in on job + run: | + echo "${{ github.event.comment.body }}" + echo "${{ github.event.commit_comment.comment.body }}" + echo "${{ github.event.commits[0].message }}" + echo "${{ github.event.commits[0].author.email }}" + echo "${{ github.event.commits[0].author.name }}" + echo "${{ github.event.discussion.body }}" + echo "${{ github.event.discussion.title }}" + echo "${{ github.event.head_commit.message }}" + echo "${{ github.event.head_commit.author.email }}" + echo "${{ github.event.head_commit.author.name }}" + echo "${{ github.event.issue.title }}" + echo "${{ github.event.issue.body }}" + echo "${{ github.event.issue_comment.comment.body }}" + echo "${{ github.event.pages[0].page_name }}" + echo "${{ github.event.pull_request.body }}" + echo "${{ github.event.pull_request.title }}" + echo "${{ github.event.pull_request.head.ref }}" + echo "${{ github.event.pull_request.head.label }}" + echo "${{ github.event.pull_request.head.repo.default_branch }}" + echo "${{ github.event.review.body }}" + echo "${{ github.head_ref }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml new file mode 100644 index 00000000000..874d0b2a1cc --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml @@ -0,0 +1,67 @@ +# Copyright 2021 OpenSSF 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. +on: + issue: + +env: + COMMENT_BODY: "${{ github.event.comment.body }}" + COMMIT_COMMENT: "${{ github.event.commit_comment.comment.body }}" + COMMIT_MESSAGE: "${{ github.event.commits[0].message }}" + COMMIT_AUTHOR_EMAIL: "${{ github.event.commits[0].author.email }}" + COMMIT_AUTHOR_NAME: "${{ github.event.commits[0].author.name }}" + DISCUSSION_BODY: ${{ github.event.discussion.body }} + DISCUSSION_TITLE: ${{ github.event.discussion.title }} + FORK_FORKEE_NAME: ${{ github.event.fork.forkee.name }} + HEAD_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + HEAD_COMMIT_AUTHOR_EMAIL: "${{ github.event.head_commit.author.email }}" + HEAD_COMMIT_AUTHOR_NAME: "${{ github.event.head_commit.author.name }}" + ISSUE_TITLE: "${{ github.event.issue.title }}" + ISSUE_BODY: "${{ github.event.issue.body }}" + ISSUE_COMMENT_COMMENT: "${{ github.event.issue_comment.comment.body }}" + PAGE_NAME: "${{ github.event.pages[0].page_name }}" + PR_BODY: "${{ github.event.pull_request.body }}" + PR_TITLE: "${{ github.event.pull_request.title }}" + PR_HEAD_REF: "${{ github.event.pull_request.head.ref }}" + PR_HEAD_LABEL: "${{ github.event.pull_request.head.label }}" + REPO_PR_DEFAULT_BRANCH: "${{ github.event.pull_request.head.repo.default_branch }}" + REVIEW_BODY: "${{ github.event.review.body }}" + HEAD_REF: "${{ github.head_ref }}" + +jobs: + pretty-generic-job: + steps: + - name: everything everywhere in on job + run: | + echo "$COMMENT_BODY" + echo "$COMMIT_COMMENT" + echo "$COMMIT_MESSAGE" + echo "$COMMIT_AUTHOR_EMAIL" + echo "$COMMIT_AUTHOR_NAME" + echo "$DISCUSSION_BODY" + echo "$DISCUSSION_TITLE" + echo "$FORK_FORKEE_NAME" + echo "$HEAD_COMMIT_MESSAGE" + echo "$HEAD_COMMIT_AUTHOR_EMAIL" + echo "$HEAD_COMMIT_AUTHOR_NAME" + echo "$ISSUE_TITLE" + echo "$ISSUE_BODY" + echo "$ISSUE_COMMENT_COMMENT" + echo "$PAGE_NAME" + echo "$PR_BODY" + echo "$PR_TITLE" + echo "$PR_HEAD_REF" + echo "$PR_HEAD_LABEL" + echo "$REPO_PR_DEFAULT_BRANCH" + echo "$REVIEW_BODY" + echo "$HEAD_REF" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml new file mode 100644 index 00000000000..0131bbf1605 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml @@ -0,0 +1,36 @@ +# Copyright 2024 OpenSSF 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. + on: [pull_request] + + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + # I make no sense + + # not really + + + # some extra tabs here to check if they're kept + + - name: Check title + run: | + if [[ ! "${{ github.event.issue.title }}" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml new file mode 100644 index 00000000000..2b49a1ed235 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 OpenSSF 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. + on: [pull_request] + + env: + ISSUE_TITLE: ${{ github.event.issue.title }} + + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + # I make no sense + + # not really + + + # some extra tabs here to check if they're kept + + - name: Check title + run: | + if [[ ! "$ISSUE_TITLE" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml new file mode 100644 index 00000000000..42fc98f9e53 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + SUPER_RELEVANT: "bla" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + if [[ ! "${{ github.event.issue.title }}" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml new file mode 100644 index 00000000000..c003ca4058f --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + SUPER_RELEVANT: "bla" + ISSUE_TITLE: ${{ github.event.issue.title }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + if [[ ! "$ISSUE_TITLE" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml new file mode 100644 index 00000000000..45e4605ea8a --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 OpenSSF 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. +on: issue_comment + +jobs: + issue_commented: + # This job only runs for issue comments + name: Issue comment + if: ${{ !github.event.issue.pull_request }} + runs-on: ubuntu-latest + steps: + - run: | + echo "Comment on issue #${{ github.event.issue.number }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml new file mode 100644 index 00000000000..db658e33d8d --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check title + run: | + if [[ ! "${{ github.event.issue.title }}" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi +# Extra line below, to test if we keep the newline on EOF diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml new file mode 100644 index 00000000000..d9e7efae69d --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + ISSUE_TITLE: ${{ github.event.issue.title }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check title + run: | + if [[ ! "$ISSUE_TITLE" =~ ^.*:\ .*$ ]]; then + echo "Bad issue title" + exit 1 + fi +# Extra line below, to test if we keep the newline on EOF diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml new file mode 100644 index 00000000000..fc8d44c3d28 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 OpenSSF 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: The anti-blanklines workflow +on: + issue: +permissions: + contents: read +jobs: + simple-job: + steps: + - name: As simple as it can be + run: | + echo "${{ github.event.comment.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml new file mode 100644 index 00000000000..04097893113 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 OpenSSF 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: The anti-blanklines workflow +on: + issue: +permissions: + contents: read +env: + COMMENT_BODY: "${{ github.event.comment.body }}" +jobs: + simple-job: + steps: + - name: As simple as it can be + run: | + echo "$COMMENT_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml new file mode 100644 index 00000000000..843d0c7118a --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml @@ -0,0 +1,58 @@ +name: Run benchmark comparison + +on: + issue_comment: + types: [created] + +permissions: read-all + +jobs: + benchmark-compare: + runs-on: ubuntu-latest + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/benchmark-compare ')}} + steps: + - uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c # v1.0 + with: + team: 'team' + organization: angular + token: ${{secrets.BENCHMARK_COMPARE_MEMBERSHIP_GITHUB_TOKEN}} + exit: true + # Indicate that the benchmark command was received. + - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3 + with: + comment-id: ${{github.event.comment.id}} + token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}' + reactions: 'rocket' + - uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1 + id: comment-branch + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + # Specify repository as the PR branch might be from a fork. + repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}} + # Checkout the pull request and assume it being trusted given we've checked + # that the action was triggered by a team member. + ref: ${{steps.comment-branch.outputs.head_ref}} + - uses: ./.github/actions/yarn-install + - uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@97cb376945b07e37671e3b38ee727ff00dc1394b + with: + bazelrc: ./.bazelrc.user + + - name: Preparing benchmark for GitHub action + id: info + run: yarn benchmarks prepare-for-github-action "${{github.event.comment.body}}" + + - run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}} + id: benchmark + name: Running benchmark + + - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3 + with: + issue-number: ${{github.event.issue.number}} + token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}' + body: | + ## Benchmark Test Results + **Test**: `${{steps.info.outputs.benchmarkTarget}}` + ### PR (${{steps.info.outputs.prHeadSha}}) + ${{steps.benchmark.outputs.workingStageResultsText}} + ### Compare Ref (${{steps.info.outputs.compareSha}}) + ${{steps.benchmark.outputs.comparisonResultsText}} \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml new file mode 100644 index 00000000000..cced3454af3 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml @@ -0,0 +1,61 @@ +name: Run benchmark comparison + +on: + issue_comment: + types: [created] + +permissions: read-all + +env: + COMMENT_BODY: ${{ github.event.comment.body }} + +jobs: + benchmark-compare: + runs-on: ubuntu-latest + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/benchmark-compare ')}} + steps: + - uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c # v1.0 + with: + team: 'team' + organization: angular + token: ${{secrets.BENCHMARK_COMPARE_MEMBERSHIP_GITHUB_TOKEN}} + exit: true + # Indicate that the benchmark command was received. + - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3 + with: + comment-id: ${{github.event.comment.id}} + token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}' + reactions: 'rocket' + - uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1 + id: comment-branch + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + # Specify repository as the PR branch might be from a fork. + repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}} + # Checkout the pull request and assume it being trusted given we've checked + # that the action was triggered by a team member. + ref: ${{steps.comment-branch.outputs.head_ref}} + - uses: ./.github/actions/yarn-install + - uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@97cb376945b07e37671e3b38ee727ff00dc1394b + with: + bazelrc: ./.bazelrc.user + + - name: Preparing benchmark for GitHub action + id: info + run: yarn benchmarks prepare-for-github-action "$COMMENT_BODY" + + - run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}} + id: benchmark + name: Running benchmark + + - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3 + with: + issue-number: ${{github.event.issue.number}} + token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}' + body: | + ## Benchmark Test Results + **Test**: `${{steps.info.outputs.benchmarkTarget}}` + ### PR (${{steps.info.outputs.prHeadSha}}) + ${{steps.benchmark.outputs.workingStageResultsText}} + ### Compare Ref (${{steps.info.outputs.compareSha}}) + ${{steps.benchmark.outputs.comparisonResultsText}} \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml new file mode 100644 index 00000000000..8c91b3653cb --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml @@ -0,0 +1,99 @@ +--- +name: apidiff + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + scan_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: main + - name: Get main commit + id: main + run: echo "hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + - name: Get changed directories + id: changed_dirs + # Ignore changes to the internal and root directories. + # Ignore added files with --diff-filter=a. + run: | + dirs=$(go run ./internal/actions/cmd/changefinder -q --diff-filter=a) + if [ -z "$dirs" ] + then + echo "skip=1" >> $GITHUB_OUTPUT + echo "No changes worth diffing!" + else + for d in $dirs; do list=${list},\"${d}\"; done + echo "changed={\"changed\":[${list#,}]}" >> $GITHUB_OUTPUT + echo "skip=" >> $GITHUB_OUTPUT + fi + outputs: + changed_dirs: ${{ steps.changed_dirs.outputs.changed }} + skip: ${{ steps.changed_dirs.outputs.skip }} + apidiff: + needs: scan_changes + runs-on: ubuntu-latest + if: "!needs.scan_changes.outputs.skip && !contains(github.event.pull_request.labels.*.name, 'breaking change allowed')" + continue-on-error: true + strategy: + matrix: ${{ fromJson(needs.scan_changes.outputs.changed_dirs) }} + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + - name: Install latest apidiff + run: go install golang.org/x/exp/cmd/apidiff@latest + - uses: actions/checkout@v3 + with: + ref: main + - name: Create baseline + id: baseline + run: | + export CHANGED=${{ matrix.changed }} + echo pkg="${CHANGED//\//_}_pkg.main" >> $GITHUB_OUTPUT + - name: Create Go package baseline + run: cd ${{ matrix.changed }} && apidiff -m -w ${{ steps.baseline.outputs.pkg }} . + - name: Upload baseline package data + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.baseline.outputs.pkg }} + path: ${{ matrix.changed }}/${{ steps.baseline.outputs.pkg }} + retention-days: 1 + - uses: actions/checkout@v3 + - name: Download baseline package data + uses: actions/download-artifact@v3 + with: + name: ${{ steps.baseline.outputs.pkg }} + path: ${{ matrix.changed }} + - name: Compare regenerated code to baseline + # Only ignore Go interface additions when the PR is from OwlBot as it is + # likely a new method added to the gRPC client stub interface, which is + # non-breaking. + run: | + cd ${{ matrix.changed }} && apidiff -m -incompatible ${{ steps.baseline.outputs.pkg }} . > diff.txt + if [[ ${{ github.event.pull_request.head.ref }} == owl-bot-copy ]]; then + sed -i '/: added/d' ./diff.txt + fi + cat diff.txt && ! [ -s diff.txt ] + + - name: Add breaking change label + if: ${{ failure() && !github.event.pull_request.head.repo.fork }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.addLabels({ + issue_number: ${{ github.event.number }}, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['breaking change'] + }) \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml new file mode 100644 index 00000000000..8b1a8ffae0e --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml @@ -0,0 +1,102 @@ +--- +name: apidiff + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +env: + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + +jobs: + scan_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: main + - name: Get main commit + id: main + run: echo "hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + - name: Get changed directories + id: changed_dirs + # Ignore changes to the internal and root directories. + # Ignore added files with --diff-filter=a. + run: | + dirs=$(go run ./internal/actions/cmd/changefinder -q --diff-filter=a) + if [ -z "$dirs" ] + then + echo "skip=1" >> $GITHUB_OUTPUT + echo "No changes worth diffing!" + else + for d in $dirs; do list=${list},\"${d}\"; done + echo "changed={\"changed\":[${list#,}]}" >> $GITHUB_OUTPUT + echo "skip=" >> $GITHUB_OUTPUT + fi + outputs: + changed_dirs: ${{ steps.changed_dirs.outputs.changed }} + skip: ${{ steps.changed_dirs.outputs.skip }} + apidiff: + needs: scan_changes + runs-on: ubuntu-latest + if: "!needs.scan_changes.outputs.skip && !contains(github.event.pull_request.labels.*.name, 'breaking change allowed')" + continue-on-error: true + strategy: + matrix: ${{ fromJson(needs.scan_changes.outputs.changed_dirs) }} + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + - name: Install latest apidiff + run: go install golang.org/x/exp/cmd/apidiff@latest + - uses: actions/checkout@v3 + with: + ref: main + - name: Create baseline + id: baseline + run: | + export CHANGED=${{ matrix.changed }} + echo pkg="${CHANGED//\//_}_pkg.main" >> $GITHUB_OUTPUT + - name: Create Go package baseline + run: cd ${{ matrix.changed }} && apidiff -m -w ${{ steps.baseline.outputs.pkg }} . + - name: Upload baseline package data + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.baseline.outputs.pkg }} + path: ${{ matrix.changed }}/${{ steps.baseline.outputs.pkg }} + retention-days: 1 + - uses: actions/checkout@v3 + - name: Download baseline package data + uses: actions/download-artifact@v3 + with: + name: ${{ steps.baseline.outputs.pkg }} + path: ${{ matrix.changed }} + - name: Compare regenerated code to baseline + # Only ignore Go interface additions when the PR is from OwlBot as it is + # likely a new method added to the gRPC client stub interface, which is + # non-breaking. + run: | + cd ${{ matrix.changed }} && apidiff -m -incompatible ${{ steps.baseline.outputs.pkg }} . > diff.txt + if [[ "$PR_HEAD_REF" == owl-bot-copy ]]; then + sed -i '/: added/d' ./diff.txt + fi + cat diff.txt && ! [ -s diff.txt ] + + - name: Add breaking change label + if: ${{ failure() && !github.event.pull_request.head.repo.fork }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.addLabels({ + issue_number: ${{ github.event.number }}, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['breaking change'] + }) \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml new file mode 100644 index 00000000000..8583efef84b --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml @@ -0,0 +1,44 @@ +name: Generate Release Image + +on: + pull_request: + paths: + - '**/CHANGELOG.md' + push: + paths: + - '**/CHANGELOG.md' + +jobs: + release-image: + if: github.repository == 'lit/lit' + name: Generate Release Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: package-lock.json + - uses: google/wireit@setup-github-actions-caching/v1 + - name: NPM install + run: npm ci + - name: Build release-image script + working-directory: packages/internal-scripts + run: npm run build + + - name: Create release image + run: | + # Store the PR body contents containing the changelog in 'release.md' + cat <<'EOF' > release.md + ${{ github.event.pull_request.body }} + EOF + # Only render the pull request content including and after the "# + # Releases" heading. + node_modules/.bin/release-image -m <(sed -n '/# Releases/,$p' release.md) + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: releaseimage + path: release.png \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml new file mode 100644 index 00000000000..d8dce2a5f4d --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml @@ -0,0 +1,47 @@ +name: Generate Release Image + +on: + pull_request: + paths: + - '**/CHANGELOG.md' + push: + paths: + - '**/CHANGELOG.md' + +env: + PR_BODY: ${{ github.event.pull_request.body }} + +jobs: + release-image: + if: github.repository == 'lit/lit' + name: Generate Release Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: package-lock.json + - uses: google/wireit@setup-github-actions-caching/v1 + - name: NPM install + run: npm ci + - name: Build release-image script + working-directory: packages/internal-scripts + run: npm run build + + - name: Create release image + run: | + # Store the PR body contents containing the changelog in 'release.md' + cat <<'EOF' > release.md + $PR_BODY + EOF + # Only render the pull request content including and after the "# + # Releases" heading. + node_modules/.bin/release-image -m <(sed -n '/# Releases/,$p' release.md) + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: releaseimage + path: release.png \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml new file mode 100644 index 00000000000..351b856fca9 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-job-level-env-vars: + env: + # Safe but unnused env var. Same name that our script would use + ISSUE_BODY: "${{ github.event.issue.body }}" + + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + PULL_REQ_BODY: ${{ github.event.pull_request.body }} + steps: + - run: | + echo "${{ github.event.issue.body }}" + - run: | + echo "${{ github.event.pull_request.body }}" + + using-step-level-env-vars: + steps: + - name: the step + env: + # Safe but unnused env var. Same name that our script would use + HEAD_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + PULL_REQ_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "${{ github.event.head_commit.message }}" + echo "${{ github.event.pull_request.title }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml new file mode 100644 index 00000000000..7fa12a97047 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-job-level-env-vars: + env: + # Safe but unnused env var. Same name that our script would use + ISSUE_BODY: "${{ github.event.issue.body }}" + + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + PULL_REQ_BODY: ${{ github.event.pull_request.body }} + steps: + - run: | + echo "$ISSUE_BODY" + - run: | + echo "$PULL_REQ_BODY" + + using-step-level-env-vars: + steps: + - name: the step + env: + # Safe but unnused env var. Same name that our script would use + HEAD_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + PULL_REQ_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "$HEAD_COMMIT_MESSAGE" + echo "$PULL_REQ_TITLE" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml new file mode 100644 index 00000000000..2e2fa7c4fa5 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +on: + issue_comment: + types: [created] + +permissions: read-all + +env: + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + TITLE: ${{github.event.issue.title}} + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "${{ github.event.issue.title }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml new file mode 100644 index 00000000000..051ea87be2c --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +on: + issue_comment: + types: [created] + +permissions: read-all + +env: + # Safe but unnused env var. Different name than the one our script would use. + # Ideally we should keep the name as it is + TITLE: ${{github.event.issue.title}} + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "$TITLE" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml new file mode 100644 index 00000000000..64998d1ee98 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml @@ -0,0 +1,70 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +# Env block intentionally not placed right above the "jobs" block, where our script usually places it +env: + # Existent but non-related env var + ISSUE-NUMBER: ${{github.event.issue.number}} + + # Safe but unnused env var. Same name that our script would use + ISSUE_COMMENT_COMMENT: "${{ github.event.issue_comment.comment.body }}" + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "$ISSUE_NUMBER" + echo "${{ github.event.issue_comment.comment.body }}" + + # content orinally not present in any env var. + # This same content will be used again and should reuse created env var + - run: | + echo "${{ github.event.discussion.body }}" + + using-job-level-env-vars: + env: + # Existent but non-related env var + NUM_COMMENTS: ${{ github.event.issue.comments }} + steps: + - run: | + echo "$NUM_COMMENTS" + + # Same variable that already was used on previous job. They should reuse the same workflow-level env var + - run: | + echo "${{ github.event.discussion.body }}" + + using-step-level-env-vars: + steps: + - name: the step + env: + # Existent but non-related env var + ISSUE_ID: ${{ github.event.issue.id }} + + run: | + echo "$ISSUE_ID" + + # Same variable that already was used on other jobs. They should reuse the same workflow-level env var + - run: | + echo "${{ github.event.discussion.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml new file mode 100644 index 00000000000..eea0182c0fb --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml @@ -0,0 +1,72 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +# Env block intentionally not placed right above the "jobs" block, where our script usually places it +env: + # Existent but non-related env var + ISSUE-NUMBER: ${{ github.event.issue.number }} + + # Safe but unnused env var. Same name that our script would use. No spaces inside brackets + ISSUE_COMMENT_COMMENT: "${{github.event.issue_comment.comment.body}}" + DISCUSSION_BODY: ${{ github.event.discussion.body }} + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "$ISSUE_NUMBER" + echo "$ISSUE_COMMENT_COMMENT" + + # content orinally not present in any env var. + # This same content will be used again and should reuse created env var + - run: | + echo "$DISCUSSION_BODY" + + using-job-level-env-vars: + env: + # Existent but non-related env var + NUM_COMMENTS: ${{ github.event.issue.comments }} + + steps: + - run: | + echo "$NUM_COMMENTS" + + # Same variable that already was used on previous job. They should reuse the same workflow-level env var + - run: | + echo "$DISCUSSION_BODY" + + using-step-level-env-vars: + steps: + - name: the step + env: + # Existent but non-related env var + ISSUE_ID: ${{ github.event.issue.id }} + + run: | + echo "$ISSUE_ID" + + # Same variable that already was used other jobs. They should reuse the same workflow-level env var + - run: | + echo "$DISCUSSION_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml new file mode 100644 index 00000000000..05828819511 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 OpenSSF 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. +on: + issue: + +jobs: + fascinating-job: + steps: + - name: it runs like magic + run: | + echo "${{ github.event.issue.title }}" + + incredible-other-job: + steps: + - name: absolutely outstanding, safe as nothing else + run: | + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml new file mode 100644 index 00000000000..d6f737f5762 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml @@ -0,0 +1,32 @@ +# Copyright 2024 OpenSSF 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. +on: + issue: + +env: + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_TITLE: "${{ github.event.issue.title }}" + +jobs: + fascinating-job: + steps: + - name: it runs like magic + run: | + echo "$ISSUE_TITLE" + + incredible-other-job: + steps: + - name: absolutely outstanding, safe as nothing else + run: | + echo "$ISSUE_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml new file mode 100644 index 00000000000..8b5627deab7 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 OpenSSF 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. +on: + discussion: + types: [created] + +jobs: + really-complete-job: + steps: + - name: it's only the beginning + run: | + echo "${{ github.event.discussion.title }}" + - name: ok, now we're talking + run: | + echo "${{ github.event.discussion.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml new file mode 100644 index 00000000000..8bca4098bf2 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 OpenSSF 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. +on: + discussion: + types: [created] + +env: + DISCUSSION_BODY: ${{ github.event.discussion.body }} + DISCUSSION_TITLE: ${{ github.event.discussion.title }} + +jobs: + really-complete-job: + steps: + - name: it's only the beginning + run: | + echo "$DISCUSSION_TITLE" + - name: ok, now we're talking + run: | + echo "$DISCUSSION_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml new file mode 100644 index 00000000000..f85081fafc5 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml @@ -0,0 +1,25 @@ +# Copyright 2024 OpenSSF 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. +on: + fork: + issue_comment: + types: [created, edited] + +jobs: + solution-to-all-repo-problems: + steps: + - name: where things are done + run: | + echo "${{ github.event.issue_comment.comment }}" + mkdir "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml new file mode 100644 index 00000000000..489dfb990b0 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 OpenSSF 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. +on: + fork: + issue_comment: + types: [created, edited] + +env: + ISSUE_COMMENT_COMMENT: ${{ github.event.issue_comment.comment }} + ISSUE_BODY: ${{ github.event.issue.body }} + +jobs: + solution-to-all-repo-problems: + steps: + - name: where things are done + run: | + echo "$ISSUE_COMMENT_COMMENT" + mkdir "$ISSUE_BODY \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml new file mode 100644 index 00000000000..6a107d155f6 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check msg + run: | + msg="${{ github.event.commits[0].message }}" + if [[ ! $msg =~ ^.*:\ .*$ ]]; then + echo "Bad message " + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml new file mode 100644 index 00000000000..cc9069f7a5f --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml @@ -0,0 +1,34 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + COMMIT_MESSAGE: ${{ github.event.commits[0].message }} + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check msg + run: | + msg="$COMMIT_MESSAGE" + if [[ ! $msg =~ ^.*:\ .*$ ]]; then + echo "Bad message " + exit 1 + fi \ No newline at end of file From 5bddd1a4ef450046c03a095896255955ed417238 Mon Sep 17 00:00:00 2001 From: Joyce Brum Date: Sat, 29 Jun 2024 04:39:28 +0000 Subject: [PATCH 04/31] Merge pull request #4 from joycebrum/feat/get-input-needed-to-generate-patch Signed-off-by: Pedro Kaj Kjellerup Nacht --- pkg/scorecard/scorecard_result.go | 6 +++++ pkg/scorecard/scorecard_test.go | 4 +++ .../impl.go | 18 ++++++++++--- .../patch/impl.go | 27 +++++-------------- .../patch/impl_test.go | 9 +++++-- ... fourSpacesIndentationExistentEnvVar.yaml} | 0 ...pacesIndentationExistentEnvVar_fixed.yaml} | 0 7 files changed, 39 insertions(+), 25 deletions(-) rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{fourSpacesIndendationExistentEnvVar.yaml => fourSpacesIndentationExistentEnvVar.yaml} (100%) rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{fourSpacesIndendationExistentEnvVar_fixed.yaml => fourSpacesIndentationExistentEnvVar_fixed.yaml} (100%) diff --git a/pkg/scorecard/scorecard_result.go b/pkg/scorecard/scorecard_result.go index 6f925092135..9d0eb2871e6 100644 --- a/pkg/scorecard/scorecard_result.go +++ b/pkg/scorecard/scorecard_result.go @@ -395,6 +395,12 @@ func assignRawData(probeCheckName string, request *checker.CheckRequest, ret *Re } func populateRawResults(request *checker.CheckRequest, probesToRun []string, ret *Result) error { + localPath, err := request.RepoClient.LocalPath() + if err != nil { + return fmt.Errorf("RepoClient.LocalPath: %w", err) + } + ret.RawResults.Metadata.Metadata["localPath"] = localPath + seen := map[string]bool{} for _, probeName := range probesToRun { p, err := proberegistration.Get(probeName) diff --git a/pkg/scorecard/scorecard_test.go b/pkg/scorecard/scorecard_test.go index 2ea5a973b1c..3435bb54966 100644 --- a/pkg/scorecard/scorecard_test.go +++ b/pkg/scorecard/scorecard_test.go @@ -242,6 +242,7 @@ func TestRun_WithProbes(t *testing.T) { "repository.name": "ossf/scorecard", "repository.sha1": "1a17bb812fb2ac23e9d09e86e122f8b67563aed7", "repository.uri": "github.com/ossf/scorecard", + "localPath": "test_path", }, }, }, @@ -279,6 +280,9 @@ func TestRun_WithProbes(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) mockRepoClient := mockrepo.NewMockRepoClient(ctrl) + mockRepoClient.EXPECT().LocalPath().DoAndReturn(func() (string, error) { + return "test_path", nil + }).AnyTimes() repo := mockrepo.NewMockRepo(ctrl) repo.EXPECT().URI().Return(tt.args.uri).AnyTimes() diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 00c60d4d7db..7dffcf5288f 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -18,6 +18,8 @@ package hasDangerousWorkflowScriptInjection import ( "embed" "fmt" + "os" + "path" "github.com/ossf/scorecard/v5/checker" "github.com/ossf/scorecard/v5/finding" @@ -54,6 +56,9 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { } var findings []finding.Finding + var curr string + var content []byte + localPath := raw.Metadata.Metadata["localPath"] for _, e := range r.Workflows { e := e if e.Type == checker.DangerousWorkflowScriptInjection { @@ -69,12 +74,19 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { LineStart: &e.File.Offset, Snippet: &e.File.Snippet, }) - findingPatch := patch.GeneratePatch(e.File) - f.WithPatch(&findingPatch) + + wp := path.Join(localPath, e.File.Path) + if curr != wp { + curr = wp + content, err = os.ReadFile(wp) + } + if err == nil { + findingPatch := patch.GeneratePatch(e.File, content) + f.WithPatch(&findingPatch) + } findings = append(findings, *f) } } - if len(findings) == 0 { return falseOutcome() } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 97e9e981ae6..786b686e738 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -26,33 +26,20 @@ func parseDiff(diff string) string { if i == -1 { return diff } - // remove everything before """\n + diff = diff[i+4:] i = strings.LastIndex(diff, "\"\"\"") if i == -1 { return diff } - // remove everything after \n \t""" + return diff[:i] } -// Placeholder function that should receive the file of a workflow and -// return the end result of the Script Injection patch -// -// TODO: Receive the dangerous workflow as parameter. -func GeneratePatch(f checker.File) string { - // TODO: Implement - // example: - // type scriptInjection - // path {.github/workflows/active-elastic-job~active-elastic-job~build.yml github.head_ref 91 0 0 1} - // snippet github.head_ref - src := `asasas -hello """ola""" - message=$(echo "${{ github.event.head_commit.message }}" | tail -n +3) -adios` - dst := `asasas -hello """ola""" - message=$(echo $COMMIT | tail -n +3) -adios` +func GeneratePatch(f checker.File, content []byte) string { + src := string(content) + // TODO: call fix method + dst := src + "\n # random change for testing patch diff" + return parseDiff(cmp.Diff(src, dst)) } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 95b517a5da1..772f3b27493 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -92,7 +92,7 @@ func Test_GeneratePatch(t *testing.T) { // by the ones in smaller scope. // Once proper behavior is implemented, enable this test // { - // name: "Reuse env var already existent on smaller scope, it convers case of same or different names", + // name: "Reuse env var already existent on smaller scope, it converts case of same or different names", // inputFilepath: "reuseEnvVarSmallerScope.yaml", // expectedFilepath: "reuseEnvVarSmallerScope_fixed.yaml", // }, @@ -139,7 +139,12 @@ func Test_GeneratePatch(t *testing.T) { t.Errorf("Couldn't read expected testfile. Error:\n%s", err) } - output := GeneratePatch(inputFile) + inputContent, err := os.ReadFile("./testdata/" + tt.inputFilepath) + if err != nil { + t.Errorf("Couldn't read input testfile. Error:\n%s", err) + } + + output := GeneratePatch(inputFile, inputContent) if diff := cmp.Diff(string(expectedContent[:]), output); diff != "" { // Uncomment the line bellow when the script is fully implemented and the tests are adapted to // the official input/output diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndendationExistentEnvVar_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml From 488d89a760b90130422f5b8758539ce16c1f9753 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Fri, 8 Mar 2024 22:50:27 +0000 Subject: [PATCH 05/31] impl.go: slight refactor to loop Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 7dffcf5288f..89c48ca5a83 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -57,35 +57,38 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { var findings []finding.Finding var curr string - var content []byte + var content string localPath := raw.Metadata.Metadata["localPath"] for _, e := range r.Workflows { e := e - if e.Type == checker.DangerousWorkflowScriptInjection { - f, err := finding.NewWith(fs, Probe, - fmt.Sprintf("script injection with untrusted input '%v'", e.File.Snippet), - nil, finding.OutcomeTrue) - if err != nil { - return nil, Probe, fmt.Errorf("create finding: %w", err) - } - f = f.WithLocation(&finding.Location{ - Path: e.File.Path, - Type: e.File.Type, - LineStart: &e.File.Offset, - Snippet: &e.File.Snippet, - }) + if e.Type != checker.DangerousWorkflowScriptInjection { + continue + } + f, err := finding.NewWith(fs, Probe, + fmt.Sprintf("script injection with untrusted input '%v'", e.File.Snippet), + nil, finding.OutcomeTrue) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + f = f.WithLocation(&finding.Location{ + Path: e.File.Path, + Type: e.File.Type, + LineStart: &e.File.Offset, + Snippet: &e.File.Snippet, + }) - wp := path.Join(localPath, e.File.Path) - if curr != wp { - curr = wp - content, err = os.ReadFile(wp) - } - if err == nil { - findingPatch := patch.GeneratePatch(e.File, content) - f.WithPatch(&findingPatch) - } - findings = append(findings, *f) + wp := path.Join(localPath, e.File.Path) + if curr != wp { + curr = wp + var c []byte + c, err = os.ReadFile(wp) + content = string(c) + } + if err == nil { + findingPatch := patch.GeneratePatch(e.File, content) + f.WithPatch(&findingPatch) } + findings = append(findings, *f) } if len(findings) == 0 { return falseOutcome() From 93c2fbad50720d1bfae3da9faf1251e04e1c324f Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Wed, 6 Mar 2024 23:03:57 +0000 Subject: [PATCH 06/31] Add envvars to existing or new env, still not replaced in `run` Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 186 +++++++++++++++++- 1 file changed, 182 insertions(+), 4 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 786b686e738..5e0e3e8b413 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -12,13 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. +/* +TODO + - Detects the end of the existing envvars at the first line that does not declare an + envvar. This can lead to weird insertion positions if there is a comment in the + middle of the `env:` block. + - Tried performing a "dumber" implementation than the Python script, with less + "parsing" of the workflow. However, the location given by f.Offset isn't precise + enough. It only marks the start of the `run:` command, not the line where the + variable is actually used. Will therefore need to, at least, parse the `run` + command to replace all the instances of the unsafe variable. This means we can + have multiple identical remediations if the same variable is used multiple times + in the same step... that's just life. +*/ package patch import ( + "fmt" + "os" + "path" + "regexp" + "slices" "strings" "github.com/google/go-cmp/cmp" + "github.com/ossf/scorecard/v5/checker" + sce "github.com/ossf/scorecard/v5/errors" +) + +const ( + assumedIndent = 2 ) func parseDiff(diff string) string { @@ -36,10 +60,164 @@ func parseDiff(diff string) string { return diff[:i] } -func GeneratePatch(f checker.File, content []byte) string { - src := string(content) - // TODO: call fix method - dst := src + "\n # random change for testing patch diff" +// Placeholder function that should receive the file of a workflow and +// return the end result of the Script Injection patch +// +// TODO: Receive the dangerous workflow as parameter. +func GeneratePatch(f checker.File) string { + // TODO: Implement + // example: + // type scriptInjection + // path {.github/workflows/active-elastic-job~active-elastic-job~build.yml github.head_ref 91 0 0 1} + // snippet github.head_ref + + // for testing, while we figure out how to get the full path + path := path.Join("/home/pnacht_google_com/temp_test", f.Path) + + blob, err := os.ReadFile(path) + + if err != nil { + return "" + } + + lines := strings.Split(string(blob), "\n") + + globalIndentation, ok := findGlobalIndentation(lines) + + if !ok { + // invalid workflow, could not determine global indentation + return "" + } + envPos, envvarIndent, exists := findExistingEnv(lines, globalIndentation) + + if !exists { + envPos, ok = findNewEnvPos(lines, globalIndentation) + + if !ok { + // invalid workflow, could not determine location for new environment + return "" + } + + label := strings.Repeat(" ", globalIndentation) + "env:" + lines = slices.Insert(lines, envPos, []string{label, ""}...) + envPos += 1 // position now points to `env:`, insert variables below it + envvarIndent = globalIndentation + assumedIndent + } + + envvar, err := convertUnsafeVarToEnvvar(f.Snippet) + if err != nil { + fmt.Printf("%v", err) + } + lines = slices.Insert(lines, envPos, strings.Repeat(" ", envvarIndent)+envvar) + + for _, line := range lines { + fmt.Println(line) + } + + src := `asasas +hello """ola""" + message=$(echo "${{ github.event.head_commit.message }}" | tail -n +3) +adios` + dst := `asasas +hello """ola""" + message=$(echo $COMMIT | tail -n +3) +adios` return parseDiff(cmp.Diff(src, dst)) } + +func findGlobalIndentation(lines []string) (int, bool) { + r := regexp.MustCompile(`^\W*on:`) + for _, line := range lines { + if r.MatchString(line) { + return len(line) - len(strings.TrimLeft(line, " ")), true + } + } + + return -1, false +} + +func findExistingEnv(lines []string, globalIndent int) (int, int, bool) { + num_lines := len(lines) + indent := strings.Repeat(" ", globalIndent) + + // regex to detect the global `env:` block + labelRegex := regexp.MustCompile(indent + "env:") + i := 0 + for i = 0; i < num_lines; i++ { + line := lines[i] + if labelRegex.MatchString(line) { + break + } + } + + if i >= num_lines-1 { + // there must be at least one more line + return -1, -1, false + } + + i++ // move to line after `env:` + envvarIndent := len(lines[i]) - len(strings.TrimLeft(lines[i], " ")) + // regex to detect envvars belonging to the global `env:` block + envvarRegex := regexp.MustCompile(indent + `\W+[^#]`) + for ; i < num_lines; i++ { + line := lines[i] + if !envvarRegex.MatchString(line) { + // no longer declaring envvars + break + } + } + + return i, envvarIndent, true +} + +func findNewEnvPos(lines []string, globalIndent int) (int, bool) { + // the new env is added right before `jobs:` + indent := strings.Repeat(" ", globalIndent) + r := regexp.MustCompile(indent + "jobs:") + for i, line := range lines { + if r.MatchString(line) { + return i, true + } + } + + return -1, false +} + +var unsafeVarToEnvvar = map[*regexp.Regexp]string{ + regexp.MustCompile(`issue\.title`): "ISSUE_TITLE", + regexp.MustCompile(`issue\.body`): "ISSUE_BODY", + regexp.MustCompile(`pull_request\.title`): "PR_TITLE", + regexp.MustCompile(`pull_request\.body`): "PR_BODY", + regexp.MustCompile(`comment\.body`): "COMMENT_BODY", + regexp.MustCompile(`review\.body`): "REVIEW_BODY", + regexp.MustCompile(`review_comment\.body`): "REVIEW_COMMENT_BODY", + regexp.MustCompile(`pages.*\.page_name`): "PAGE_NAME", + regexp.MustCompile(`commits.*\.message`): "COMMIT_MESSAGE", + regexp.MustCompile(`head_commit\.message`): "COMMIT_MESSAGE", + regexp.MustCompile(`head_commit\.author\.email`): "AUTHOR_EMAIL", + regexp.MustCompile(`head_commit\.author\.name`): "AUTHOR_NAME", + regexp.MustCompile(`commits.*\.author\.email`): "AUTHOR_EMAIL", + regexp.MustCompile(`commits.*\.author\.name`): "AUTHOR_NAME", + regexp.MustCompile(`pull_request\.head\.ref`): "PR_HEAD_REF", + regexp.MustCompile(`pull_request\.head\.label`): "PR_HEAD_LABEL", + regexp.MustCompile(`pull_request\.head\.repo\.default_branch`): "PR_DEFAULT_BRANCH", + regexp.MustCompile(`github\.head_ref`): "HEAD_REF", +} + +func convertUnsafeVarToEnvvar(unsafeVar string) (string, error) { + for regex, envvar := range unsafeVarToEnvvar { + if regex.MatchString(unsafeVar) { + return fmt.Sprintf("%s: %s", envvar, unsafeVar), nil + } + } + return "", sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf( + "Detected unsafe variable '%s', but could not find a compatible envvar name", + unsafeVar)) +} + +func replaceUnsafeVarWithEnvvar(line string, unsafeVar string, envvar string) string { + r := regexp.MustCompile(`${{\W*` + unsafeVar + `\W*}}`) + return r.ReplaceAllString(line, "$"+envvar) +} From 0394b860182ae06091b3bc88d5993f649c5bc5ed Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 7 Mar 2024 22:54:05 +0000 Subject: [PATCH 07/31] Replace unsafe variables in run commands, generate git diff Git diff created using hexops/gotextdiff, WHICH IS ARCHIVED. It is unfortunately the only package I found which could do it. To be discussed with Scorecard maintainers whether it's worth it. Signed-off-by: Pedro Kaj Kjellerup Nacht --- go.mod | 1 + go.sum | 2 + .../patch/impl.go | 206 ++++++++++-------- 3 files changed, 119 insertions(+), 90 deletions(-) diff --git a/go.mod b/go.mod index bed402b2d64..c20c087c584 100644 --- a/go.mod +++ b/go.mod @@ -160,6 +160,7 @@ require ( github.com/google/wire v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/hexops/gotextdiff v1.0.3 github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect diff --git a/go.sum b/go.sum index d068368ee3b..8a469b4c9ed 100644 --- a/go.sum +++ b/go.sum @@ -478,6 +478,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 5e0e3e8b413..458ee8ce528 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -29,108 +29,88 @@ package patch import ( "fmt" - "os" - "path" "regexp" "slices" "strings" - "github.com/google/go-cmp/cmp" - "github.com/ossf/scorecard/v5/checker" - sce "github.com/ossf/scorecard/v5/errors" + + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" + "github.com/hexops/gotextdiff/span" ) const ( assumedIndent = 2 ) -func parseDiff(diff string) string { - i := strings.Index(diff, "\"\"\"\n") - if i == -1 { - return diff - } - - diff = diff[i+4:] - i = strings.LastIndex(diff, "\"\"\"") - if i == -1 { - return diff - } - - return diff[:i] -} +func GeneratePatch(f checker.File, content string) string { + unsafeVar := strings.Trim(f.Snippet, " ") + runCmdIndex := f.Offset - 1 -// Placeholder function that should receive the file of a workflow and -// return the end result of the Script Injection patch -// -// TODO: Receive the dangerous workflow as parameter. -func GeneratePatch(f checker.File) string { - // TODO: Implement - // example: - // type scriptInjection - // path {.github/workflows/active-elastic-job~active-elastic-job~build.yml github.head_ref 91 0 0 1} - // snippet github.head_ref + lines := strings.Split(string(content), "\n") - // for testing, while we figure out how to get the full path - path := path.Join("/home/pnacht_google_com/temp_test", f.Path) + unsafePattern, envvar, ok := getReplacementRegexAndEnvvarName(unsafeVar) + if !ok { + return "" + } - blob, err := os.ReadFile(path) + replaceUnsafeVarWithEnvvar(lines, unsafePattern, envvar, runCmdIndex) - if err != nil { + lines, ok = addEnvvarsToGlobalEnv(lines, envvar, unsafeVar) + if !ok { return "" } - lines := strings.Split(string(blob), "\n") + fixedWorkflow := strings.Join(lines, "\n") + return getDiff(f.Path, content, fixedWorkflow) +} + +func addEnvvarsToGlobalEnv(lines []string, envvar string, unsafeVar string) ([]string, bool) { globalIndentation, ok := findGlobalIndentation(lines) if !ok { // invalid workflow, could not determine global indentation - return "" + return nil, false } envPos, envvarIndent, exists := findExistingEnv(lines, globalIndentation) if !exists { - envPos, ok = findNewEnvPos(lines, globalIndentation) - + lines, envPos, ok = addNewGlobalEnv(lines, globalIndentation) if !ok { - // invalid workflow, could not determine location for new environment - return "" + return nil, ok } - label := strings.Repeat(" ", globalIndentation) + "env:" - lines = slices.Insert(lines, envPos, []string{label, ""}...) - envPos += 1 // position now points to `env:`, insert variables below it + // position now points to `env:`, insert variables below it + envPos += 1 envvarIndent = globalIndentation + assumedIndent } + envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", envvar, unsafeVar) + lines = slices.Insert(lines, envPos, + strings.Repeat(" ", envvarIndent)+envvarDefinition) + return lines, ok +} - envvar, err := convertUnsafeVarToEnvvar(f.Snippet) - if err != nil { - fmt.Printf("%v", err) - } - lines = slices.Insert(lines, envPos, strings.Repeat(" ", envvarIndent)+envvar) +func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, bool) { + envPos, ok := findNewEnvPos(lines, globalIndentation) - for _, line := range lines { - fmt.Println(line) + if !ok { + // invalid workflow, could not determine location for new environment + return nil, envPos, ok } - src := `asasas -hello """ola""" - message=$(echo "${{ github.event.head_commit.message }}" | tail -n +3) -adios` - dst := `asasas -hello """ola""" - message=$(echo $COMMIT | tail -n +3) -adios` - return parseDiff(cmp.Diff(src, dst)) + label := strings.Repeat(" ", globalIndentation) + "env:" + lines = slices.Insert(lines, envPos, []string{label, ""}...) + return lines, envPos, ok } func findGlobalIndentation(lines []string) (int, bool) { r := regexp.MustCompile(`^\W*on:`) for _, line := range lines { if r.MatchString(line) { - return len(line) - len(strings.TrimLeft(line, " ")), true + return getIndent(line), true } } @@ -157,7 +137,7 @@ func findExistingEnv(lines []string, globalIndent int) (int, int, bool) { } i++ // move to line after `env:` - envvarIndent := len(lines[i]) - len(strings.TrimLeft(lines[i], " ")) + envvarIndent := getIndent(lines[i]) // regex to detect envvars belonging to the global `env:` block envvarRegex := regexp.MustCompile(indent + `\W+[^#]`) for ; i < num_lines; i++ { @@ -184,40 +164,86 @@ func findNewEnvPos(lines []string, globalIndent int) (int, bool) { return -1, false } -var unsafeVarToEnvvar = map[*regexp.Regexp]string{ - regexp.MustCompile(`issue\.title`): "ISSUE_TITLE", - regexp.MustCompile(`issue\.body`): "ISSUE_BODY", - regexp.MustCompile(`pull_request\.title`): "PR_TITLE", - regexp.MustCompile(`pull_request\.body`): "PR_BODY", - regexp.MustCompile(`comment\.body`): "COMMENT_BODY", - regexp.MustCompile(`review\.body`): "REVIEW_BODY", - regexp.MustCompile(`review_comment\.body`): "REVIEW_COMMENT_BODY", - regexp.MustCompile(`pages.*\.page_name`): "PAGE_NAME", - regexp.MustCompile(`commits.*\.message`): "COMMIT_MESSAGE", - regexp.MustCompile(`head_commit\.message`): "COMMIT_MESSAGE", - regexp.MustCompile(`head_commit\.author\.email`): "AUTHOR_EMAIL", - regexp.MustCompile(`head_commit\.author\.name`): "AUTHOR_NAME", - regexp.MustCompile(`commits.*\.author\.email`): "AUTHOR_EMAIL", - regexp.MustCompile(`commits.*\.author\.name`): "AUTHOR_NAME", - regexp.MustCompile(`pull_request\.head\.ref`): "PR_HEAD_REF", - regexp.MustCompile(`pull_request\.head\.label`): "PR_HEAD_LABEL", - regexp.MustCompile(`pull_request\.head\.repo\.default_branch`): "PR_DEFAULT_BRANCH", - regexp.MustCompile(`github\.head_ref`): "HEAD_REF", +type unsafePattern struct { + envvarName string + idRegex *regexp.Regexp + replaceRegex *regexp.Regexp } -func convertUnsafeVarToEnvvar(unsafeVar string) (string, error) { - for regex, envvar := range unsafeVarToEnvvar { - if regex.MatchString(unsafeVar) { - return fmt.Sprintf("%s: %s", envvar, unsafeVar), nil +func newUnsafePattern(e, p string) unsafePattern { + return unsafePattern{ + envvarName: e, + idRegex: regexp.MustCompile(p), + replaceRegex: regexp.MustCompile(`{{\W*.*?` + p + `.*?\W*}}`), + } +} + +var unsafePatterns = []unsafePattern{ + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), + newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), + newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), + newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), + newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), + newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), + newUnsafePattern("PR_DEFAULT_BRANCH", `github\.event\.pull_request\.head\.repo\.default_branch`), + newUnsafePattern("PR_HEAD_LABEL", `github\.event\.pull_request\.head\.label`), + newUnsafePattern("PR_HEAD_REF", `github\.event\.pull_request\.head\.ref`), + newUnsafePattern("PR_TITLE", `github\.event\.pull_request\.title`), + newUnsafePattern("REVIEW_BODY", `github\.event\.review\.body`), + newUnsafePattern("REVIEW_COMMENT_BODY", `github\.event\.review_comment\.body`), + + newUnsafePattern("HEAD_REF", `github\.head_ref`), +} + +func getReplacementRegexAndEnvvarName(unsafeVar string) (*regexp.Regexp, string, bool) { + for _, p := range unsafePatterns { + if p.idRegex.MatchString(unsafeVar) { + return p.replaceRegex, p.envvarName, true } } - return "", sce.WithMessage(sce.ErrScorecardInternal, - fmt.Sprintf( - "Detected unsafe variable '%s', but could not find a compatible envvar name", - unsafeVar)) + return nil, "", false +} + +func replaceUnsafeVarWithEnvvar(lines []string, replaceRegex *regexp.Regexp, envvar string, runIndex uint) { + runIndent := getIndent(lines[runIndex]) + for i := int(runIndex); i < len(lines) && isParentLevelIndent(lines[i], runIndent); i++ { + lines[i] = replaceRegex.ReplaceAllString(lines[i], envvar) + } } -func replaceUnsafeVarWithEnvvar(line string, unsafeVar string, envvar string) string { - r := regexp.MustCompile(`${{\W*` + unsafeVar + `\W*}}`) - return r.ReplaceAllString(line, "$"+envvar) +func getIndent(line string) int { + return len(line) - len(strings.TrimLeft(line, " -")) +} + +func isBlankOrComment(line string) bool { + blank := regexp.MustCompile(`^\W*$`) + comment := regexp.MustCompile(`^\W*#`) + + return blank.MatchString(line) || comment.MatchString(line) +} + +func isParentLevelIndent(line string, parentIndent int) bool { + if isBlankOrComment(line) { + return false + } + return getIndent(line) >= parentIndent +} + +// gets the changes as a git-diff. Following the standard used in git diff, the +// path to the "old" version is prefixed with a/, and the "new" with b/: +// +// --- a/.github/workflows/foo.yml +// +++ b/.github/workflows/foo.yml +// @@ -42,13 +42,22 @@ +// ... +func getDiff(path, original, patched string) string { + edits := myers.ComputeEdits(span.URIFromPath(path), original, patched) + aPath := "a/" + path + bPath := "b/" + path + return fmt.Sprint(gotextdiff.ToUnified(aPath, bPath, original, edits)) } From 3c7f9c61e8d1ff6c8f931de30f8f76e820494753 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Fri, 8 Mar 2024 22:49:14 +0000 Subject: [PATCH 08/31] Rewrite test file - Test patchWorkflow instead of GeneratePatch. This avoids the complication of comparing diff files; we can instead simply compare the output workflow to an expected "fixed" workflow. - Examples with multiple findings must have separate "fixed" workflows for each finding, not a single file which covers all findings - Instead of hard-coding the finding details (snippet, line position), run raw.DangerousWorkflow() to get that data automatically. This does make these tests a bit more "integration-test-like", but makes them substantially easier to maintain. Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl_test.go | 255 +++++++++++++----- .../patch/testdata/realExample1.diff | 23 ++ 2 files changed, 204 insertions(+), 74 deletions(-) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 772f3b27493..2c0aafbfa0c 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -15,78 +15,91 @@ package patch import ( + "context" + "fmt" + "io" "os" + "path" + "regexp" + "slices" + "strings" "testing" + "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/ossf/scorecard/v5/checker" + "github.com/ossf/scorecard/v5/checks/raw" + mockrepo "github.com/ossf/scorecard/v5/clients/mockclients" ) -func Test_GeneratePatch(t *testing.T) { +const ( + testDir = "./testdata" +) + +func Test_patchWorkflow(t *testing.T) { t.Parallel() tests := []struct { - name string - inputFilepath string - expectedFilepath string - // err error + name string + filePath string }{ - // Extracted from real Angular fix: https://github.com/angular/angular/pull/51026/files { - name: "Real Example 1", - inputFilepath: "realExample1.yaml", - expectedFilepath: "realExample1_fixed.yaml", + // Extracted from real Angular fix: https://github.com/angular/angular/pull/51026/files + name: "Real Example 1", + filePath: "realExample1.yaml", + }, + { + // Inspired on a real fix: https://github.com/googleapis/google-cloud-go/pull/9011/files + name: "Real Example 2", + filePath: "realExample2.yaml", + }, + { + // Inspired from a real lit/lit fix: https://github.com/lit/lit/pull/3669/files + name: "Real Example 3", + filePath: "realExample3.yaml", }, - // Inspired on a real fix: https://github.com/googleapis/google-cloud-go/pull/9011/files { - name: "Real Example 2", - inputFilepath: "realExample2.yaml", - expectedFilepath: "realExample2_fixed.yaml", + name: "User's input is assigned to a variable before used", + filePath: "userInputAssignedToVariable.yaml", }, - // Inspired from a real lit/lit fix: https://github.com/lit/lit/pull/3669/files { - name: "Real Example 3", - inputFilepath: "realExample3.yaml", - expectedFilepath: "realExample3_fixed.yaml", + name: "Two incidences in different jobs", + filePath: "twoInjectionsDifferentJobs.yaml", }, { - name: "Test all (or most) types of user input that should be detected", - inputFilepath: "allKindsOfUserInput.yaml", - expectedFilepath: "allKindsOfUserInput_fixed.yaml", + name: "Two incidences in same job", + filePath: "twoInjectionsSameJob.yaml", }, { - name: "User's input is assigned to a variable before used", - inputFilepath: "userInputAssignedToVariable.yaml", - expectedFilepath: "userInputAssignedToVariable_fixed.yaml", + name: "Two incidences in same step", + filePath: "twoInjectionsSameStep.yaml", }, { - name: "Two incidences in different jobs", - inputFilepath: "twoInjectionsDifferentJobs.yaml", - expectedFilepath: "twoInjectionsDifferentJobs_fixed.yaml", + name: "4-spaces indentation is kept the same", + filePath: "fourSpacesIndentationExistentEnvVar.yaml", }, { - name: "Two incidences in same job", - inputFilepath: "twoInjectionsSameJob.yaml", - expectedFilepath: "twoInjectionsSameJob_fixed.yaml", + name: "Crazy but valid indentation is kept the same", + filePath: "crazyButValidIndentation.yaml", }, { - name: "Two incidences in same step", - inputFilepath: "twoInjectionsSameStep.yaml", - expectedFilepath: "twoInjectionsSameStep_fixed.yaml", + name: "Newline on EOF is kept", + filePath: "newlineOnEOF.yaml", }, { - name: "Reuse existent workflow level env var, if has the same name we'd give", - inputFilepath: "reuseWorkflowLevelEnvVars.yaml", - expectedFilepath: "reuseWorkflowLevelEnvVars_fixed.yaml", + name: "Ignore if user input regex is just part of a comment", + filePath: "ignorePatternInsideComments.yaml", + }, + { + name: "Reuse existent workflow level env var, if has the same name we'd give", + filePath: "reuseWorkflowLevelEnvVars.yaml", }, // Test currently failing because we don't look for existent env vars pointing to the same content. // Once proper behavior is implemented, enable this test // { // name: "Reuse existent workflow level env var, if it DOES NOT have the same name we'd give", // inputFilepath: "reuseEnvVarWithDiffName.yaml", - // expectedFilepath: "reuseEnvVarWithDiffName_fixed.yaml", // }, - // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed // by the ones in smaller scope. @@ -94,63 +107,157 @@ func Test_GeneratePatch(t *testing.T) { // { // name: "Reuse env var already existent on smaller scope, it converts case of same or different names", // inputFilepath: "reuseEnvVarSmallerScope.yaml", - // expectedFilepath: "reuseEnvVarSmallerScope_fixed.yaml", // }, - { - name: "4-spaces indentation is kept the same", - inputFilepath: "fourSpacesIndentationExistentEnvVar.yaml", - expectedFilepath: "fourSpacesIndentationExistentEnvVar_fixed.yaml", - }, - { - name: "Crazy but valid indentation is kept the same", - inputFilepath: "crazyButValidIndentation.yaml", - expectedFilepath: "crazyButValidIndentation_fixed.yaml", - }, - { - name: "Newline on EOF is kept", - inputFilepath: "newlineOnEOF.yaml", - expectedFilepath: "newlineOnEOF_fixed.yaml", - }, - // Test currently failing due to lack of style awareness. Currently we always add a blankline after + // Test currently failing due to lack of style awareness. Currently we always add a blank line after // the env block. // Once proper behavior is implemented, enable this test. // { - // name: "Keep style if file doesnt use blank lines between blocks", + // name: "Keep style if file doesn't use blank lines between blocks", // inputFilepath: "noLineBreaksBetweenBlocks.yaml", // expectedFilepath: "noLineBreaksBetweenBlocks_fixed.yaml", // }, - { - name: "Ignore if user input regex is just part of a comment", - inputFilepath: "ignorePatternInsideComments.yaml", - expectedFilepath: "ignorePatternInsideComments.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() - inputFile := checker.File{ - Path: tt.inputFilepath, - } + dws := detectDangerousWorkflows(tt.filePath, t) - expectedContent, err := os.ReadFile("./testdata/" + tt.expectedFilepath) + inputContent, err := os.ReadFile(path.Join(testDir, tt.filePath)) if err != nil { - t.Errorf("Couldn't read expected testfile. Error:\n%s", err) + t.Errorf("Couldn't read input test file. Error:\n%s", err) } - inputContent, err := os.ReadFile("./testdata/" + tt.inputFilepath) - if err != nil { - t.Errorf("Couldn't read input testfile. Error:\n%s", err) - } + numFindings := len(dws) + + for i, dw := range dws { + if dw.Type == checker.DangerousWorkflowUntrustedCheckout { + // Patching not yet implemented + continue + } + + // Only used for error messages, increment by 1 for human legibility of + // errors + i = i + 1 + + output, err := patchWorkflow(dw.File, string(inputContent)) + if err != nil { + t.Errorf("Couldn't patch workflow for finding #%d.", i) + } - output := GeneratePatch(inputFile, inputContent) - if diff := cmp.Diff(string(expectedContent[:]), output); diff != "" { - // Uncomment the line bellow when the script is fully implemented and the tests are adapted to - // the official input/output + // build path to fixed version + dot := strings.LastIndex(tt.filePath, ".") + fixedPath := tt.filePath[:dot] + "_fixed" + if numFindings > 1 { + fixedPath = fmt.Sprintf("%s_%d", fixedPath, i) + } + fixedPath = fixedPath + tt.filePath[dot:] - // t.Errorf("mismatch (-want +got):\n%s", diff) + expectedContent, err := os.ReadFile(path.Join(testDir, fixedPath)) + if err != nil { + t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", i, err) + } + expected := string(expectedContent) + + if diff := cmp.Diff(expected, output); diff != "" { + t.Errorf("mismatch for finding #%d. (-want +got):\n%s", i, diff) + } } + }) } } + +func detectDangerousWorkflows(filePath string, t *testing.T) []checker.DangerousWorkflow { + ctrl := gomock.NewController(t) + mockRepoClient := mockrepo.NewMockRepoClient(ctrl) + mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return( + // Pretend the file is in the workflow directory to pass a check deep in + // raw.DangerousWorkflow + []string{path.Join(".github/workflows/", filePath)}, nil, + ) + mockRepoClient.EXPECT().GetFileReader(gomock.Any()).DoAndReturn(func(file string) (io.ReadCloser, error) { + return os.Open("./testdata/" + filePath) + }).AnyTimes() + + req := &checker.CheckRequest{ + Ctx: context.Background(), + RepoClient: mockRepoClient, + } + + dw, err := raw.DangerousWorkflow(req) + + if err != nil { + t.Errorf("Error running raw.DangerousWorkflow. Error:\n%s", err) + } + + // Sort findings by position. This ensures each finding is compared to its + // respective "fixed" workflow. + slices.SortFunc(dw.Workflows, func(a, b checker.DangerousWorkflow) int { + aPos := a.File.Offset + bPos := b.File.Offset + if aPos < bPos { + return -1 + } + if aPos > bPos { + return +1 + } + return 0 + }) + + return dw.Workflows +} + +// This function parses the diff file and makes a few changes necessary to make a +// valid comparison with the output of GeneratePatch. +// +// For example, the following diff file created with `git diff`: +// +// diff --git a/testdata/foo.yaml b/testdata/foo_fixed.yaml +// index 843d0c71..cced3454 100644 +// --- a/testdata/foo.yaml +// +++ b/testdata/foo_fixed.yaml +// @@ -6,6 +6,9 @@ jobs: +// < ... the diff ... > +// +// becomes: +// +// --- a/testdata/foo.yaml +// +++ b/testdata/foo_fixed.yaml +// @@ -6,6 +6,9 @@ +// < ... the diff ... > +// +// Note that, despite the differences between our output and the official +// `git diff`, our output is still valid and can be passed to +// `patch -p1 < path/to/file.diff` to apply the fix to the workflow. +func parseDiffFile(filepath string) (string, error) { + c, err := os.ReadFile(path.Join("./testdata", filepath)) + if err != nil { + return "", err + } + + // The real `git diff` includes multiple "headers" (`diff --git ...`, `index ...`) + // Our diff does not include these headers; it starts with the "in/out" headers of + // --- a/path/to/file + // +++ b/path/to/file + // We must therefore remove any previous headers from the `git diff`. + lines := strings.Split(string(c), "\n") + i := 0 + var line string + for i, line = range lines { + if strings.HasPrefix(line, "--- ") { + break + } + } + content := strings.Join(lines[i:], "\n") + + // The real `git diff` adds contents after the `@@` anchors (the text of the line on + // which the anchor is placed): + // i.e. `@@ 1,2 3,4 @@ jobs:` + // while ours does not + // i.e. `@@ 1,2 3,4 @@` + // We must therefore remove that extra content to compare with our diff. + r := regexp.MustCompile(`(@@[ \d,+-]+@@).*`) + return r.ReplaceAllString(string(content), "$1"), nil +} diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff new file mode 100644 index 00000000000..1a5e1d664c9 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff @@ -0,0 +1,23 @@ +diff --git a/testdata/realExample1.yaml b/testdata/realExample1_fixed.yaml +index 843d0c71..cced3454 100644 +--- a/testdata/realExample1.yaml ++++ b/testdata/realExample1_fixed.yaml +@@ -6,6 +6,9 @@ on: + + permissions: read-all + ++env: ++ COMMENT_BODY: ${{ github.event.comment.body }} ++ + jobs: + benchmark-compare: + runs-on: ubuntu-latest +@@ -39,7 +42,7 @@ jobs: + + - name: Preparing benchmark for GitHub action + id: info +- run: yarn benchmarks prepare-for-github-action "${{github.event.comment.body}}" ++ run: yarn benchmarks prepare-for-github-action "$COMMENT_BODY" + + - run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}} + id: benchmark From b299a47bc400d7814751ccad42e54c3258af4011 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Mon, 1 Apr 2024 02:01:44 +0000 Subject: [PATCH 09/31] Rewrite patch/impl.go - misc refactors - use go-git to generate diff - Most functions now return errors instead of bools. This can be later used for simpler logging - Existing environment variables are now detected by parsing the files as GH workflows. This is WIP to handle existing envvars in our patches. - Remove instances of C-style for-loops, unnecessarily dangerous! - Fixed proper detection of existing env, handling blank lines and comments. Signed-off-by: Pedro Kaj Kjellerup Nacht --- go.mod | 2 +- go.sum | 2 - .../patch/impl.go | 336 +++++++++++++----- 3 files changed, 254 insertions(+), 86 deletions(-) diff --git a/go.mod b/go.mod index c20c087c584..ef50d3d4c2b 100644 --- a/go.mod +++ b/go.mod @@ -150,7 +150,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.17.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-billy/v5 v5.5.0 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 8a469b4c9ed..d068368ee3b 100644 --- a/go.sum +++ b/go.sum @@ -478,8 +478,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 458ee8ce528..cdb38ae8367 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -13,7 +13,7 @@ // limitations under the License. /* -TODO +TODO: - Detects the end of the existing envvars at the first line that does not declare an envvar. This can lead to weird insertion positions if there is a comment in the middle of the `env:` block. @@ -24,75 +24,88 @@ TODO command to replace all the instances of the unsafe variable. This means we can have multiple identical remediations if the same variable is used multiple times in the same step... that's just life. + - Handle array inputs (i.e. workflow using `github.event.commits[0]` and + `github.event.commits[1]`, which would duplicate $COMMIT_MESSAGE). + - Handle use of synonyms (i.e `commits.*?\.author\.email` and + `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). + - Handle cases where an existing envvar has the same name as what we'd use, but a + different definition. + - Handle cases where an existing envvar (regardless of name) already covers our + dangerous variable, but wasn't used. + - Move actionlint.Parse() to the calling function (once per file) */ package patch import ( + "errors" "fmt" "regexp" "slices" "strings" + "github.com/rhysd/actionlint" + "github.com/ossf/scorecard/v5/checker" + "github.com/ossf/scorecard/v5/checks/fileparser" - "github.com/hexops/gotextdiff" - "github.com/hexops/gotextdiff/myers" - "github.com/hexops/gotextdiff/span" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" ) const ( assumedIndent = 2 ) -func GeneratePatch(f checker.File, content string) string { +var ( + ymlLabelRegex = regexp.MustCompile(`^\s*[^#]:`) +) + +// Fixes the script injection identified by the finding and returns a unified diff +// users can apply (with `git apply` or `patch`) to fix the workflow themselves. +// Should an error occur, it is handled and an empty patch is returned. +func GeneratePatch(f checker.File, content string) (string, error) { + patchedWorkflow, err := patchWorkflow(f, content) + if err != nil { + return "", err + } + return getDiff(f.Path, content, patchedWorkflow) +} + +// Returns a patched version of the workflow without the script injection finding. +func patchWorkflow(f checker.File, content string) (string, error) { unsafeVar := strings.Trim(f.Snippet, " ") runCmdIndex := f.Offset - 1 lines := strings.Split(string(content), "\n") - unsafePattern, envvar, ok := getReplacementRegexAndEnvvarName(unsafeVar) + unsafePattern, ok := getUnsafePattern(unsafeVar) if !ok { - return "" + // TODO: return meaningful error for logging, even if we don't throw it. + return "", errors.New("AAA") } - replaceUnsafeVarWithEnvvar(lines, unsafePattern, envvar, runCmdIndex) - - lines, ok = addEnvvarsToGlobalEnv(lines, envvar, unsafeVar) - if !ok { - return "" + workflow, errs := actionlint.Parse([]byte(content)) + if len(errs) > 0 && workflow == nil { + return "", fileparser.FormatActionlintError(errs) } - fixedWorkflow := strings.Join(lines, "\n") - - return getDiff(f.Path, content, fixedWorkflow) -} + envvars := parseExistingEnvvars(workflow) -func addEnvvarsToGlobalEnv(lines []string, envvar string, unsafeVar string) ([]string, bool) { - globalIndentation, ok := findGlobalIndentation(lines) + lines = replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) + lines, ok = addEnvvarsToGlobalEnv(lines, envvars, unsafePattern, unsafeVar) if !ok { - // invalid workflow, could not determine global indentation - return nil, false + // TODO: return meaningful error for logging, even if we don't throw it. + return "", errors.New("AAA") } - envPos, envvarIndent, exists := findExistingEnv(lines, globalIndentation) - - if !exists { - lines, envPos, ok = addNewGlobalEnv(lines, globalIndentation) - if !ok { - return nil, ok - } - - // position now points to `env:`, insert variables below it - envPos += 1 - envvarIndent = globalIndentation + assumedIndent - } - envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", envvar, unsafeVar) - lines = slices.Insert(lines, envPos, - strings.Repeat(" ", envvarIndent)+envvarDefinition) - return lines, ok + return strings.Join(lines, "\n"), nil } +// Adds a new global environment to a workflow. Assumes a global environment does not +// yet exist. func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, bool) { envPos, ok := findNewEnvPos(lines, globalIndentation) @@ -106,8 +119,10 @@ func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, bool return lines, envPos, ok } +// Identifies the "global" indentation, as defined by the indentation on the required +// `on:` block. Will equal 0 in almost all cases. func findGlobalIndentation(lines []string) (int, bool) { - r := regexp.MustCompile(`^\W*on:`) + r := regexp.MustCompile(`^\s*on:`) for _, line := range lines { if r.MatchString(line) { return getIndent(line), true @@ -117,40 +132,58 @@ func findGlobalIndentation(lines []string) (int, bool) { return -1, false } -func findExistingEnv(lines []string, globalIndent int) (int, int, bool) { +// Detects whether a global `env:` block already exists. +// +// Returns: +// - int: the index for the line where the `env:` block is declared +// - int: the indentation used for the declared environment variables +// +// The first two values return -1 if the `env` block doesn't exist +func findExistingEnv(lines []string, globalIndent int) (int, int) { num_lines := len(lines) indent := strings.Repeat(" ", globalIndent) // regex to detect the global `env:` block labelRegex := regexp.MustCompile(indent + "env:") - i := 0 - for i = 0; i < num_lines; i++ { - line := lines[i] + + var currPos int + var line string + for currPos, line = range lines { if labelRegex.MatchString(line) { break } } - if i >= num_lines-1 { + if currPos >= num_lines-1 { // there must be at least one more line - return -1, -1, false + return -1, -1 } - i++ // move to line after `env:` - envvarIndent := getIndent(lines[i]) - // regex to detect envvars belonging to the global `env:` block - envvarRegex := regexp.MustCompile(indent + `\W+[^#]`) - for ; i < num_lines; i++ { - line := lines[i] - if !envvarRegex.MatchString(line) { + currPos++ // move to line after `env:` + insertPos := currPos // mark the position where new envvars will be added + envvarIndent := getIndent(lines[currPos]) + for i, line := range lines[currPos:] { + if isBlankOrComment(line) { + continue + } + + if isParentLevelIndent(line, globalIndent) { // no longer declaring envvars break } + + insertPos = currPos + i + 1 } - return i, envvarIndent, true + return insertPos, envvarIndent } +// Identifies the line where a new `env:` block should be inserted: right above the +// `jobs:` label. +// +// Returns: +// - int: the index for the line where the `env:` block should be inserted +// - bool: whether the `jobs:` block was found. Should always be `true` func findNewEnvPos(lines []string, globalIndent int) (int, bool) { // the new env is added right before `jobs:` indent := strings.Repeat(" ", globalIndent) @@ -174,8 +207,80 @@ func newUnsafePattern(e, p string) unsafePattern { return unsafePattern{ envvarName: e, idRegex: regexp.MustCompile(p), - replaceRegex: regexp.MustCompile(`{{\W*.*?` + p + `.*?\W*}}`), + replaceRegex: regexp.MustCompile(`{{\s*.*?` + p + `.*?\s*}}`), + } +} + +func getUnsafePattern(unsafeVar string) (unsafePattern, bool) { + for _, p := range unsafePatterns { + if p.idRegex.MatchString(unsafeVar) { + p := p + return p, true + } + } + return unsafePattern{}, false +} + +// Adds the necessary environment variable to the global `env:` block, if it exists. +// If the `env:` block does not exist, it is created right above the `jobs:` label. +func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pattern unsafePattern, unsafeVar string) ([]string, bool) { + globalIndentation, ok := findGlobalIndentation(lines) + if !ok { + // invalid workflow, could not determine global indentation + return nil, false + } + + var insertPos, envvarIndent int + if len(existingEnvvars) > 0 { + insertPos, envvarIndent = findExistingEnv(lines, globalIndentation) + } else { + lines, insertPos, ok = addNewGlobalEnv(lines, globalIndentation) + if !ok { + return nil, ok + } + + // position now points to `env:`, insert variables below it + insertPos += 1 + envvarIndent = globalIndentation + assumedIndent + } + + envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) + lines = slices.Insert(lines, insertPos, + strings.Repeat(" ", envvarIndent)+envvarDefinition, + ) + + return lines, ok +} + +func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { + envvars := make(map[string]string) + + if workflow.Env == nil { + return envvars + } + + for _, v := range workflow.Env.Vars { + envvars[v.Name.Value] = v.Value.Value + } + + return envvars +} + +// Replaces all instances of the given script injection variable with the safe +// environment variable. +func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) []string { + runIndent := getIndent(lines[runIndex]) + for i, line := range lines[runIndex:] { + currLine := int(runIndex) + i + if i > 0 && isParentLevelIndent(lines[currLine], runIndent) { + // anything at the same indent as the first line of the `- run:` block will + // mean the end of the run block. + break + } + lines[currLine] = pattern.replaceRegex.ReplaceAllString(line, pattern.envvarName) } + + return lines } var unsafePatterns = []unsafePattern{ @@ -184,8 +289,11 @@ var unsafePatterns = []unsafePattern{ newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), + newUnsafePattern("COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), + newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), + newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), @@ -200,50 +308,112 @@ var unsafePatterns = []unsafePattern{ newUnsafePattern("HEAD_REF", `github\.head_ref`), } -func getReplacementRegexAndEnvvarName(unsafeVar string) (*regexp.Regexp, string, bool) { - for _, p := range unsafePatterns { - if p.idRegex.MatchString(unsafeVar) { - return p.replaceRegex, p.envvarName, true - } - } - return nil, "", false -} - -func replaceUnsafeVarWithEnvvar(lines []string, replaceRegex *regexp.Regexp, envvar string, runIndex uint) { - runIndent := getIndent(lines[runIndex]) - for i := int(runIndex); i < len(lines) && isParentLevelIndent(lines[i], runIndent); i++ { - lines[i] = replaceRegex.ReplaceAllString(lines[i], envvar) - } -} - +// Returns the indentation of the given line. The indentation is all whitespace and +// dashes before a key or value. func getIndent(line string) int { return len(line) - len(strings.TrimLeft(line, " -")) } func isBlankOrComment(line string) bool { - blank := regexp.MustCompile(`^\W*$`) - comment := regexp.MustCompile(`^\W*#`) + blank := regexp.MustCompile(`^\s*$`) + comment := regexp.MustCompile(`^\s*#`) return blank.MatchString(line) || comment.MatchString(line) } +// Returns whether the given line is at the same indentation level as the parent scope. +// For example, when walking through the document, parsing `job_foo`: +// +// job_foo: +// runs-on: ubuntu-latest # looping over these lines, we have +// uses: ./actions/foo # parent_indent = 2 (job_foo's indentation) +// ... # we know these lines belong to job_foo because +// ... # they all have indent = 4 +// job_bar: # this line has job_foo's indentation, so we know job_foo is done +// +// Blank lines and those containing only comments are ignored and always return False. func isParentLevelIndent(line string, parentIndent int) bool { if isBlankOrComment(line) { return false } - return getIndent(line) >= parentIndent + return getIndent(line) <= parentIndent } -// gets the changes as a git-diff. Following the standard used in git diff, the -// path to the "old" version is prefixed with a/, and the "new" with b/: -// -// --- a/.github/workflows/foo.yml -// +++ b/.github/workflows/foo.yml -// @@ -42,13 +42,22 @@ -// ... -func getDiff(path, original, patched string) string { - edits := myers.ComputeEdits(span.URIFromPath(path), original, patched) - aPath := "a/" + path - bPath := "b/" + path - return fmt.Sprint(gotextdiff.ToUnified(aPath, bPath, original, edits)) +// Returns the changes between the original and patched workflows as a unified diff +// (the same generated by `git diff` or `diff -u`). +func getDiff(path, original, patched string) (string, error) { + // initialize an in-memory repository + repo, err := newInMemoryRepo() + if err != nil { + return "", err + } + + // commit original workflow to in-memory repository + originalCommit, err := commitWorkflow(path, original, repo) + if err != nil { + return "", err + } + + // commit patched workflow to in-memory repository + patchedCommit, err := commitWorkflow(path, patched, repo) + if err != nil { + return "", err + } + + return toUnifiedDiff(originalCommit, patchedCommit) +} + +// Initializes an in-memory repository +func newInMemoryRepo() (*git.Repository, error) { + // initialize an in-memory repository + filesystem := memfs.New() + repo, err := git.Init(memory.NewStorage(), filesystem) + if err != nil { + return nil, err + } + + return repo, nil +} + +// Commits the workflow at the given path to the in-memory repository +func commitWorkflow(path, contents string, repo *git.Repository) (*object.Commit, error) { + worktree, err := repo.Worktree() + if err != nil { + return nil, err + } + filesystem := worktree.Filesystem + + // create (or overwrite) file + df, err := filesystem.Create(path) + if err != nil { + return nil, err + } + + df.Write([]byte(contents)) + df.Close() + + // commit file to in-memory repository + worktree.Add(path) + hash, err := worktree.Commit("x", &git.CommitOptions{}) + if err != nil { + return nil, err + } + + commit, err := repo.CommitObject(hash) + if err != nil { + return nil, err + } + return commit, nil +} + +// Returns a unified diff describing the difference between the given commits +func toUnifiedDiff(originalCommit, patchedCommit *object.Commit) (string, error) { + patch, err := originalCommit.Patch(patchedCommit) + if err != nil { + return "", err + } + builder := strings.Builder{} + patch.Encode(&builder) + + return builder.String(), nil } From 9a5e04308e3dc6d088d11351f5c716def6d89b20 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Fri, 28 Jun 2024 21:37:44 +0000 Subject: [PATCH 10/31] Update test workflows - Fix inconsistencies between original and "fixed" versions - Store multiple "fixed" workflows for tests with multiple findings. Each "fixed" workflow fixes a single finding. The files are numbered according to the order in which the findings are found by moving down the file. - allKindsOfUserInput removed. Would require too many "fixed" workflows to test. The behavior can be tested more directly. Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/testdata/allKindsOfUserInput.yaml | 42 ------------ .../testdata/allKindsOfUserInput_fixed.yaml | 67 ------------------- .../patch/testdata/realExample2_fixed.yaml | 2 +- .../patch/testdata/realExample3_fixed.yaml | 2 +- .../testdata/reuseWorkflowLevelEnvVars.yaml | 30 +++------ ...=> reuseWorkflowLevelEnvVars_fixed_1.yaml} | 30 ++------- .../reuseWorkflowLevelEnvVars_fixed_2.yaml | 57 ++++++++++++++++ .../reuseWorkflowLevelEnvVars_fixed_3.yaml | 57 ++++++++++++++++ .../twoInjectionsDifferentJobs_fixed_1.yaml | 31 +++++++++ ...> twoInjectionsDifferentJobs_fixed_2.yaml} | 5 +- .../patch/testdata/twoInjectionsSameJob.yaml | 4 +- ...yaml => twoInjectionsSameJob_fixed_1.yaml} | 7 +- .../twoInjectionsSameJob_fixed_2.yaml | 29 ++++++++ .../patch/testdata/twoInjectionsSameStep.yaml | 2 +- .../twoInjectionsSameStep_fixed_1.yaml | 28 ++++++++ ...aml => twoInjectionsSameStep_fixed_2.yaml} | 5 +- 16 files changed, 229 insertions(+), 169 deletions(-) delete mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml delete mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{reuseWorkflowLevelEnvVars_fixed.yaml => reuseWorkflowLevelEnvVars_fixed_1.yaml} (67%) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{twoInjectionsDifferentJobs_fixed.yaml => twoInjectionsDifferentJobs_fixed_2.yaml} (91%) rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{twoInjectionsSameJob_fixed.yaml => twoInjectionsSameJob_fixed_1.yaml} (81%) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{twoInjectionsSameStep_fixed.yaml => twoInjectionsSameStep_fixed_2.yaml} (85%) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml deleted file mode 100644 index c18f9995c47..00000000000 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 OpenSSF 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. -on: - issue: - -jobs: - pretty-generic-job: - steps: - - name: everything everywhere in on job - run: | - echo "${{ github.event.comment.body }}" - echo "${{ github.event.commit_comment.comment.body }}" - echo "${{ github.event.commits[0].message }}" - echo "${{ github.event.commits[0].author.email }}" - echo "${{ github.event.commits[0].author.name }}" - echo "${{ github.event.discussion.body }}" - echo "${{ github.event.discussion.title }}" - echo "${{ github.event.head_commit.message }}" - echo "${{ github.event.head_commit.author.email }}" - echo "${{ github.event.head_commit.author.name }}" - echo "${{ github.event.issue.title }}" - echo "${{ github.event.issue.body }}" - echo "${{ github.event.issue_comment.comment.body }}" - echo "${{ github.event.pages[0].page_name }}" - echo "${{ github.event.pull_request.body }}" - echo "${{ github.event.pull_request.title }}" - echo "${{ github.event.pull_request.head.ref }}" - echo "${{ github.event.pull_request.head.label }}" - echo "${{ github.event.pull_request.head.repo.default_branch }}" - echo "${{ github.event.review.body }}" - echo "${{ github.head_ref }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml deleted file mode 100644 index 874d0b2a1cc..00000000000 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/allKindsOfUserInput_fixed.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2021 OpenSSF 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. -on: - issue: - -env: - COMMENT_BODY: "${{ github.event.comment.body }}" - COMMIT_COMMENT: "${{ github.event.commit_comment.comment.body }}" - COMMIT_MESSAGE: "${{ github.event.commits[0].message }}" - COMMIT_AUTHOR_EMAIL: "${{ github.event.commits[0].author.email }}" - COMMIT_AUTHOR_NAME: "${{ github.event.commits[0].author.name }}" - DISCUSSION_BODY: ${{ github.event.discussion.body }} - DISCUSSION_TITLE: ${{ github.event.discussion.title }} - FORK_FORKEE_NAME: ${{ github.event.fork.forkee.name }} - HEAD_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - HEAD_COMMIT_AUTHOR_EMAIL: "${{ github.event.head_commit.author.email }}" - HEAD_COMMIT_AUTHOR_NAME: "${{ github.event.head_commit.author.name }}" - ISSUE_TITLE: "${{ github.event.issue.title }}" - ISSUE_BODY: "${{ github.event.issue.body }}" - ISSUE_COMMENT_COMMENT: "${{ github.event.issue_comment.comment.body }}" - PAGE_NAME: "${{ github.event.pages[0].page_name }}" - PR_BODY: "${{ github.event.pull_request.body }}" - PR_TITLE: "${{ github.event.pull_request.title }}" - PR_HEAD_REF: "${{ github.event.pull_request.head.ref }}" - PR_HEAD_LABEL: "${{ github.event.pull_request.head.label }}" - REPO_PR_DEFAULT_BRANCH: "${{ github.event.pull_request.head.repo.default_branch }}" - REVIEW_BODY: "${{ github.event.review.body }}" - HEAD_REF: "${{ github.head_ref }}" - -jobs: - pretty-generic-job: - steps: - - name: everything everywhere in on job - run: | - echo "$COMMENT_BODY" - echo "$COMMIT_COMMENT" - echo "$COMMIT_MESSAGE" - echo "$COMMIT_AUTHOR_EMAIL" - echo "$COMMIT_AUTHOR_NAME" - echo "$DISCUSSION_BODY" - echo "$DISCUSSION_TITLE" - echo "$FORK_FORKEE_NAME" - echo "$HEAD_COMMIT_MESSAGE" - echo "$HEAD_COMMIT_AUTHOR_EMAIL" - echo "$HEAD_COMMIT_AUTHOR_NAME" - echo "$ISSUE_TITLE" - echo "$ISSUE_BODY" - echo "$ISSUE_COMMENT_COMMENT" - echo "$PAGE_NAME" - echo "$PR_BODY" - echo "$PR_TITLE" - echo "$PR_HEAD_REF" - echo "$PR_HEAD_LABEL" - echo "$REPO_PR_DEFAULT_BRANCH" - echo "$REVIEW_BODY" - echo "$HEAD_REF" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml index 8b1a8ffae0e..b9958c80215 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml @@ -84,7 +84,7 @@ jobs: # non-breaking. run: | cd ${{ matrix.changed }} && apidiff -m -incompatible ${{ steps.baseline.outputs.pkg }} . > diff.txt - if [[ "$PR_HEAD_REF" == owl-bot-copy ]]; then + if [[ $PR_HEAD_REF == owl-bot-copy ]]; then sed -i '/: added/d' ./diff.txt fi cat diff.txt && ! [ -s diff.txt ] diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml index d8dce2a5f4d..56c434770f1 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml @@ -9,7 +9,7 @@ on: - '**/CHANGELOG.md' env: - PR_BODY: ${{ github.event.pull_request.body }} + PR_BODY: ${{ github.event.pull_request.body }} jobs: release-image: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml index 64998d1ee98..bc035386161 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml @@ -16,10 +16,10 @@ name: Run benchmark comparison # Env block intentionally not placed right above the "jobs" block, where our script usually places it env: # Existent but non-related env var - ISSUE-NUMBER: ${{github.event.issue.number}} + ISSUE_NUMBER: ${{github.event.issue.number}} - # Safe but unnused env var. Same name that our script would use - ISSUE_COMMENT_COMMENT: "${{ github.event.issue_comment.comment.body }}" + # Safe but unnused env var. Same name that our script would use. No spaces inside brackets + COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" on: issue_comment: @@ -35,13 +35,13 @@ jobs: using-workflow-level-env-vars: steps: - run: | - echo "$ISSUE_NUMBER" - echo "${{ github.event.issue_comment.comment.body }}" + echo "$ISSUE_NUMBER" + echo "${{ github.event.issue_comment.comment.body }}" # content orinally not present in any env var. # This same content will be used again and should reuse created env var - run: | - echo "${{ github.event.discussion.body }}" + echo "${{ github.event.issue.body }}" using-job-level-env-vars: env: @@ -49,22 +49,8 @@ jobs: NUM_COMMENTS: ${{ github.event.issue.comments }} steps: - run: | - echo "$NUM_COMMENTS" + echo "$NUM_COMMENTS" # Same variable that already was used on previous job. They should reuse the same workflow-level env var - run: | - echo "${{ github.event.discussion.body }}" - - using-step-level-env-vars: - steps: - - name: the step - env: - # Existent but non-related env var - ISSUE_ID: ${{ github.event.issue.id }} - - run: | - echo "$ISSUE_ID" - - # Same variable that already was used on other jobs. They should reuse the same workflow-level env var - - run: | - echo "${{ github.event.discussion.body }}" \ No newline at end of file + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml similarity index 67% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml index eea0182c0fb..f449c9dcd6f 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml @@ -16,11 +16,10 @@ name: Run benchmark comparison # Env block intentionally not placed right above the "jobs" block, where our script usually places it env: # Existent but non-related env var - ISSUE-NUMBER: ${{ github.event.issue.number }} + ISSUE_NUMBER: ${{github.event.issue.number}} # Safe but unnused env var. Same name that our script would use. No spaces inside brackets - ISSUE_COMMENT_COMMENT: "${{github.event.issue_comment.comment.body}}" - DISCUSSION_BODY: ${{ github.event.discussion.body }} + COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" on: issue_comment: @@ -36,37 +35,22 @@ jobs: using-workflow-level-env-vars: steps: - run: | - echo "$ISSUE_NUMBER" - echo "$ISSUE_COMMENT_COMMENT" + echo "$ISSUE_NUMBER" + echo "$COMMENT_BODY" # content orinally not present in any env var. # This same content will be used again and should reuse created env var - run: | - echo "$DISCUSSION_BODY" + echo "${{ github.event.issue.body }}" using-job-level-env-vars: env: # Existent but non-related env var NUM_COMMENTS: ${{ github.event.issue.comments }} - steps: - run: | - echo "$NUM_COMMENTS" + echo "$NUM_COMMENTS" # Same variable that already was used on previous job. They should reuse the same workflow-level env var - run: | - echo "$DISCUSSION_BODY" - - using-step-level-env-vars: - steps: - - name: the step - env: - # Existent but non-related env var - ISSUE_ID: ${{ github.event.issue.id }} - - run: | - echo "$ISSUE_ID" - - # Same variable that already was used other jobs. They should reuse the same workflow-level env var - - run: | - echo "$DISCUSSION_BODY" \ No newline at end of file + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml new file mode 100644 index 00000000000..de202d49027 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml @@ -0,0 +1,57 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +# Env block intentionally not placed right above the "jobs" block, where our script usually places it +env: + # Existent but non-related env var + ISSUE_NUMBER: ${{github.event.issue.number}} + + # Safe but unnused env var. Same name that our script would use. No spaces inside brackets + COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_BODY: ${{ github.event.issue.body }} + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "$ISSUE_NUMBER" + echo "${{ github.event.issue_comment.comment.body }}" + + # content orinally not present in any env var. + # This same content will be used again and should reuse created env var + - run: | + echo "$ISSUE_BODY" + + using-job-level-env-vars: + env: + # Existent but non-related env var + NUM_COMMENTS: ${{ github.event.issue.comments }} + steps: + - run: | + echo "$NUM_COMMENTS" + + # Same variable that already was used on previous job. They should reuse the same workflow-level env var + - run: | + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml new file mode 100644 index 00000000000..369170760cd --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml @@ -0,0 +1,57 @@ +# Copyright 2024 OpenSSF 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: Run benchmark comparison + +# Env block intentionally not placed right above the "jobs" block, where our script usually places it +env: + # Existent but non-related env var + ISSUE_NUMBER: ${{github.event.issue.number}} + + # Safe but unnused env var. Same name that our script would use. No spaces inside brackets + COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_BODY: ${{ github.event.issue.body }} + +on: + issue_comment: + types: [created] + issue: + types: [created] + pull_request: + types: [created] + +permissions: read-all + +jobs: + using-workflow-level-env-vars: + steps: + - run: | + echo "$ISSUE_NUMBER" + echo "${{ github.event.issue_comment.comment.body }}" + + # content orinally not present in any env var. + # This same content will be used again and should reuse created env var + - run: | + echo "${{ github.event.issue.body }}" + + using-job-level-env-vars: + env: + # Existent but non-related env var + NUM_COMMENTS: ${{ github.event.issue.comments }} + steps: + - run: | + echo "$NUM_COMMENTS" + + # Same variable that already was used on previous job. They should reuse the same workflow-level env var + - run: | + echo "$ISSUE_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml new file mode 100644 index 00000000000..38ee58ad65b --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 OpenSSF 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. +on: + issue: + +env: + ISSUE_TITLE: ${{ github.event.issue.title }} + +jobs: + fascinating-job: + steps: + - name: it runs like magic + run: | + echo "$ISSUE_TITLE" + + incredible-other-job: + steps: + - name: absolutely outstanding, safe as nothing else + run: | + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml similarity index 91% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml index d6f737f5762..b7ae07c6769 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml @@ -13,17 +13,16 @@ # limitations under the License. on: issue: - + env: ISSUE_BODY: ${{ github.event.issue.body }} - ISSUE_TITLE: "${{ github.event.issue.title }}" jobs: fascinating-job: steps: - name: it runs like magic run: | - echo "$ISSUE_TITLE" + echo "${{ github.event.issue.title }}" incredible-other-job: steps: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml index 8b5627deab7..c6c8281cb71 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: it's only the beginning run: | - echo "${{ github.event.discussion.title }}" + echo "${{ github.event.issue.title }}" - name: ok, now we're talking run: | - echo "${{ github.event.discussion.body }}" \ No newline at end of file + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_1.yaml similarity index 81% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_1.yaml index 8bca4098bf2..bf90d068d04 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_1.yaml @@ -16,15 +16,14 @@ on: types: [created] env: - DISCUSSION_BODY: ${{ github.event.discussion.body }} - DISCUSSION_TITLE: ${{ github.event.discussion.title }} + ISSUE_TITLE: ${{ github.event.issue.title }} jobs: really-complete-job: steps: - name: it's only the beginning run: | - echo "$DISCUSSION_TITLE" + echo "$ISSUE_TITLE" - name: ok, now we're talking run: | - echo "$DISCUSSION_BODY" \ No newline at end of file + echo "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml new file mode 100644 index 00000000000..dea3256ee38 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 OpenSSF 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. +on: + discussion: + types: [created] + +env: + ISSUE_BODY: ${{ github.event.issue.body }} + +jobs: + really-complete-job: + steps: + - name: it's only the beginning + run: | + echo "${{ github.event.issue.title }}" + - name: ok, now we're talking + run: | + echo "$ISSUE_BODY" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml index f85081fafc5..1bdb8597cc2 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml @@ -21,5 +21,5 @@ jobs: steps: - name: where things are done run: | - echo "${{ github.event.issue_comment.comment }}" + echo "${{ github.event.issue.title }}" mkdir "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml new file mode 100644 index 00000000000..8d47ce26273 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 OpenSSF 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. +on: + fork: + issue_comment: + types: [created, edited] + +env: + ISSUE_TITLE: ${{ github.event.issue.title }} + +jobs: + solution-to-all-repo-problems: + steps: + - name: where things are done + run: | + echo "$ISSUE_TITLE" + mkdir "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml similarity index 85% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml index 489dfb990b0..91769eee6e7 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml @@ -17,7 +17,6 @@ on: types: [created, edited] env: - ISSUE_COMMENT_COMMENT: ${{ github.event.issue_comment.comment }} ISSUE_BODY: ${{ github.event.issue.body }} jobs: @@ -25,5 +24,5 @@ jobs: steps: - name: where things are done run: | - echo "$ISSUE_COMMENT_COMMENT" - mkdir "$ISSUE_BODY \ No newline at end of file + echo "${{ github.event.issue.title }}" + mkdir "$ISSUE_BODY" \ No newline at end of file From 3f8e2af42d5cc87f120cd4ed2a77dfc0cfdee7e6 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Fri, 28 Jun 2024 23:24:07 +0000 Subject: [PATCH 11/31] Use existing envvars, validate patched workflow - If an envvar with our name and value already existed but simply wasn't used, the patch no longer duplicates it. - After the patched workflow is created, we validate that it is valid. Or, at least did not introduce any syntax errors that were not present in the original workflow. Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 20 +++- .../patch/impl.go | 88 ++++++++++++--- .../patch/impl_test.go | 100 ++++++------------ 3 files changed, 121 insertions(+), 87 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 89c48ca5a83..527afc553b8 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -27,6 +27,7 @@ import ( "github.com/ossf/scorecard/v5/internal/probes" "github.com/ossf/scorecard/v5/probes/hasDangerousWorkflowScriptInjection/patch" "github.com/ossf/scorecard/v5/probes/internal/utils/uerror" + "github.com/rhysd/actionlint" ) func init() { @@ -58,6 +59,8 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { var findings []finding.Finding var curr string var content string + var workflow *actionlint.Workflow + var errs []*actionlint.Error localPath := raw.Metadata.Metadata["localPath"] for _, e := range r.Workflows { e := e @@ -76,19 +79,28 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { LineStart: &e.File.Offset, Snippet: &e.File.Snippet, }) + findings = append(findings, *f) wp := path.Join(localPath, e.File.Path) if curr != wp { curr = wp var c []byte c, err = os.ReadFile(wp) + if err != nil { + continue + } content = string(c) + + workflow, errs = actionlint.Parse([]byte(content)) + if len(errs) > 0 && workflow == nil { + continue + } } - if err == nil { - findingPatch := patch.GeneratePatch(e.File, content) - f.WithPatch(&findingPatch) + findingPatch, err := patch.GeneratePatch(e.File, content, workflow, errs) + if err != nil { + continue } - findings = append(findings, *f) + f.WithPatch(&findingPatch) } if len(findings) == 0 { return falseOutcome() diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index cdb38ae8367..b8ad36a567d 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -30,7 +30,7 @@ TODO: `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). - Handle cases where an existing envvar has the same name as what we'd use, but a different definition. - - Handle cases where an existing envvar (regardless of name) already covers our + - Handle cases where an existing envvar with a different name already covers our dangerous variable, but wasn't used. - Move actionlint.Parse() to the calling function (once per file) */ @@ -65,16 +65,20 @@ var ( // Fixes the script injection identified by the finding and returns a unified diff // users can apply (with `git apply` or `patch`) to fix the workflow themselves. // Should an error occur, it is handled and an empty patch is returned. -func GeneratePatch(f checker.File, content string) (string, error) { - patchedWorkflow, err := patchWorkflow(f, content) +func GeneratePatch(f checker.File, content string, workflow *actionlint.Workflow, workflowErrs []*actionlint.Error) (string, error) { + patchedWorkflow, err := patchWorkflow(f, content, workflow) if err != nil { return "", err } + errs := validatePatchedWorkflow(patchedWorkflow, workflowErrs) + if len(errs) > 0 { + return "", fileparser.FormatActionlintError(errs) + } return getDiff(f.Path, content, patchedWorkflow) } // Returns a patched version of the workflow without the script injection finding. -func patchWorkflow(f checker.File, content string) (string, error) { +func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow) (string, error) { unsafeVar := strings.Trim(f.Snippet, " ") runCmdIndex := f.Offset - 1 @@ -86,11 +90,6 @@ func patchWorkflow(f checker.File, content string) (string, error) { return "", errors.New("AAA") } - workflow, errs := actionlint.Parse([]byte(content)) - if len(errs) > 0 && workflow == nil { - return "", fileparser.FormatActionlintError(errs) - } - envvars := parseExistingEnvvars(workflow) lines = replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) @@ -227,7 +226,12 @@ func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pa globalIndentation, ok := findGlobalIndentation(lines) if !ok { // invalid workflow, could not determine global indentation - return nil, false + return lines, false + } + + if _, ok = existingEnvvars[unsafeVar]; ok { + // necessary envvar was already defined + return lines, ok } var insertPos, envvarIndent int @@ -236,7 +240,7 @@ func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pa } else { lines, insertPos, ok = addNewGlobalEnv(lines, globalIndentation) if !ok { - return nil, ok + return lines, ok } // position now points to `env:`, insert variables below it @@ -249,7 +253,7 @@ func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pa strings.Repeat(" ", envvarIndent)+envvarDefinition, ) - return lines, ok + return lines, true } func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { @@ -259,8 +263,22 @@ func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { return envvars } + r := regexp.MustCompile(`\$\{\{\s*(github\.[^\s]*?)\s*}}`) for _, v := range workflow.Env.Vars { - envvars[v.Name.Value] = v.Value.Value + value := v.Value.Value + + if strings.Contains(value, "${{") { + // extract simple variable definition (without brackets, etc) + m := r.FindStringSubmatch(value) + if len(m) == 2 { + value = m[1] + envvars[value] = v.Name.Value + } else { + envvars[v.Value.Value] = v.Name.Value + } + } else { + envvars[v.Value.Value] = v.Name.Value + } } return envvars @@ -339,6 +357,50 @@ func isParentLevelIndent(line string, parentIndent int) bool { return getIndent(line) <= parentIndent } +func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) []*actionlint.Error { + _, patchedErrs := actionlint.Parse([]byte(content)) + if len(patchedErrs) == 0 { + return []*actionlint.Error{} + } + if len(originalErrs) == 0 { + return patchedErrs + } + + normalizeMsg := func(msg string) string { + // one of the error messages contains line metadata that may legitimately change + // after a patch. Only looking at the errors' first sentence eliminates this. + return strings.Split(msg, ".")[0] + } + + var newErrs []*actionlint.Error + + o := 0 + orig := originalErrs[o] + origMsg := normalizeMsg(orig.Message) + + for _, patched := range patchedErrs { + if o == len(originalErrs) { + // no more errors in the original workflow, must be an error from our patch + newErrs = append(newErrs, patched) + continue + } + + msg := normalizeMsg(patched.Message) + if orig.Column == patched.Column && orig.Kind == patched.Kind && origMsg == msg { + // Matched error, therefore not due to our patch. + o++ + if o < len(originalErrs) { + orig = originalErrs[o] + origMsg = normalizeMsg(orig.Message) + } + } else { + newErrs = append(newErrs, patched) + } + } + + return newErrs +} + // Returns the changes between the original and patched workflows as a unified diff // (the same generated by `git diff` or `diff -u`). func getDiff(path, original, patched string) (string, error) { diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 2c0aafbfa0c..7d3934f9f79 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -20,13 +20,13 @@ import ( "io" "os" "path" - "regexp" "slices" "strings" "testing" "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" + "github.com/rhysd/actionlint" "github.com/ossf/scorecard/v5/checker" "github.com/ossf/scorecard/v5/checks/raw" @@ -129,36 +129,33 @@ func Test_patchWorkflow(t *testing.T) { t.Errorf("Couldn't read input test file. Error:\n%s", err) } - numFindings := len(dws) + workflow, inputErrs := actionlint.Parse(inputContent) + if len(inputErrs) > 0 && workflow == nil { + t.Errorf("Couldn't parse file as workflow. Error:\n%s", inputErrs[0]) + } + numFindings := len(dws) for i, dw := range dws { + i = i + 1 // Only used for error messages, increment for legibility + if dw.Type == checker.DangerousWorkflowUntrustedCheckout { - // Patching not yet implemented - continue + t.Errorf("Patching of untrusted checkout (finding #%dis not implemented.", i) } - // Only used for error messages, increment by 1 for human legibility of - // errors - i = i + 1 - - output, err := patchWorkflow(dw.File, string(inputContent)) + output, err := patchWorkflow(dw.File, string(inputContent), workflow) if err != nil { t.Errorf("Couldn't patch workflow for finding #%d.", i) } - // build path to fixed version - dot := strings.LastIndex(tt.filePath, ".") - fixedPath := tt.filePath[:dot] + "_fixed" - if numFindings > 1 { - fixedPath = fmt.Sprintf("%s_%d", fixedPath, i) + patchedErrs := validatePatchedWorkflow(output, inputErrs) + if len(patchedErrs) > 0 { + t.Errorf("Patched workflow for finding #%d is invalid. Error:\n%s", i, patchedErrs[0]) } - fixedPath = fixedPath + tt.filePath[dot:] - expectedContent, err := os.ReadFile(path.Join(testDir, fixedPath)) + expected, err := getExpected(tt.filePath, numFindings, i) if err != nil { t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", i, err) } - expected := string(expectedContent) if diff := cmp.Diff(expected, output); diff != "" { t.Errorf("mismatch for finding #%d. (-want +got):\n%s", i, diff) @@ -169,6 +166,22 @@ func Test_patchWorkflow(t *testing.T) { } } +func getExpected(filePath string, numFindings, findingIndex int) (string, error) { + // build path to fixed version + dot := strings.LastIndex(filePath, ".") + fixedPath := filePath[:dot] + "_fixed" + if numFindings > 1 { + fixedPath = fmt.Sprintf("%s_%d", fixedPath, findingIndex) + } + fixedPath = fixedPath + filePath[dot:] + + content, err := os.ReadFile(path.Join(testDir, fixedPath)) + if err != nil { + return "", err + } + return string(content), nil +} + func detectDangerousWorkflows(filePath string, t *testing.T) []checker.DangerousWorkflow { ctrl := gomock.NewController(t) mockRepoClient := mockrepo.NewMockRepoClient(ctrl) @@ -208,56 +221,3 @@ func detectDangerousWorkflows(filePath string, t *testing.T) []checker.Dangerous return dw.Workflows } - -// This function parses the diff file and makes a few changes necessary to make a -// valid comparison with the output of GeneratePatch. -// -// For example, the following diff file created with `git diff`: -// -// diff --git a/testdata/foo.yaml b/testdata/foo_fixed.yaml -// index 843d0c71..cced3454 100644 -// --- a/testdata/foo.yaml -// +++ b/testdata/foo_fixed.yaml -// @@ -6,6 +6,9 @@ jobs: -// < ... the diff ... > -// -// becomes: -// -// --- a/testdata/foo.yaml -// +++ b/testdata/foo_fixed.yaml -// @@ -6,6 +6,9 @@ -// < ... the diff ... > -// -// Note that, despite the differences between our output and the official -// `git diff`, our output is still valid and can be passed to -// `patch -p1 < path/to/file.diff` to apply the fix to the workflow. -func parseDiffFile(filepath string) (string, error) { - c, err := os.ReadFile(path.Join("./testdata", filepath)) - if err != nil { - return "", err - } - - // The real `git diff` includes multiple "headers" (`diff --git ...`, `index ...`) - // Our diff does not include these headers; it starts with the "in/out" headers of - // --- a/path/to/file - // +++ b/path/to/file - // We must therefore remove any previous headers from the `git diff`. - lines := strings.Split(string(c), "\n") - i := 0 - var line string - for i, line = range lines { - if strings.HasPrefix(line, "--- ") { - break - } - } - content := strings.Join(lines[i:], "\n") - - // The real `git diff` adds contents after the `@@` anchors (the text of the line on - // which the anchor is placed): - // i.e. `@@ 1,2 3,4 @@ jobs:` - // while ours does not - // i.e. `@@ 1,2 3,4 @@` - // We must therefore remove that extra content to compare with our diff. - r := regexp.MustCompile(`(@@[ \d,+-]+@@).*`) - return r.ReplaceAllString(string(content), "$1"), nil -} From 8b47fdd4c14e5ed4f7029d432b68d92156832879 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Sat, 29 Jun 2024 00:04:02 +0000 Subject: [PATCH 12/31] Test for same injection in same step, leading to duplicate findings Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl_test.go | 14 +++++++++++--- .../patch/testdata/twoInjectionsSameStep.yaml | 1 + .../testdata/twoInjectionsSameStep_fixed_1.yaml | 1 + ...d_2.yaml => twoInjectionsSameStep_fixed_3.yaml} | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) rename probes/hasDangerousWorkflowScriptInjection/patch/testdata/{twoInjectionsSameStep_fixed_2.yaml => twoInjectionsSameStep_fixed_3.yaml} (94%) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 7d3934f9f79..a7b73379486 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -40,8 +40,9 @@ const ( func Test_patchWorkflow(t *testing.T) { t.Parallel() tests := []struct { - name string - filePath string + name string + filePath string + duplicates map[int]int // mark findings as duplicates of others, use same fix }{ { // Extracted from real Angular fix: https://github.com/angular/angular/pull/51026/files @@ -73,6 +74,9 @@ func Test_patchWorkflow(t *testing.T) { { name: "Two incidences in same step", filePath: "twoInjectionsSameStep.yaml", + duplicates: map[int]int{ + 2: 1, // finding #2 is a duplicate of #1 + }, }, { name: "4-spaces indentation is kept the same", @@ -139,7 +143,7 @@ func Test_patchWorkflow(t *testing.T) { i = i + 1 // Only used for error messages, increment for legibility if dw.Type == checker.DangerousWorkflowUntrustedCheckout { - t.Errorf("Patching of untrusted checkout (finding #%dis not implemented.", i) + t.Errorf("Patching of untrusted checkout (finding #%d) is not implemented.", i) } output, err := patchWorkflow(dw.File, string(inputContent), workflow) @@ -152,6 +156,10 @@ func Test_patchWorkflow(t *testing.T) { t.Errorf("Patched workflow for finding #%d is invalid. Error:\n%s", i, patchedErrs[0]) } + if dup, ok := tt.duplicates[i]; ok { + i = dup + } + expected, err := getExpected(tt.filePath, numFindings, i) if err != nil { t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", i, err) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml index 1bdb8597cc2..3de1f7a96db 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml @@ -21,5 +21,6 @@ jobs: steps: - name: where things are done run: | + echo "${{ github.event.issue.title }}" echo "${{ github.event.issue.title }}" mkdir "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml index 8d47ce26273..41ff00d0744 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml @@ -24,5 +24,6 @@ jobs: steps: - name: where things are done run: | + echo "$ISSUE_TITLE" echo "$ISSUE_TITLE" mkdir "${{ github.event.issue.body }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_3.yaml similarity index 94% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml rename to probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_3.yaml index 91769eee6e7..cf61c7ca06b 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_2.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_3.yaml @@ -24,5 +24,6 @@ jobs: steps: - name: where things are done run: | + echo "${{ github.event.issue.title }}" echo "${{ github.event.issue.title }}" mkdir "$ISSUE_BODY" \ No newline at end of file From c632590e6ae88b6cf6deaeb1923729ee34a7ae54 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Sat, 29 Jun 2024 00:44:24 +0000 Subject: [PATCH 13/31] Use existing envvars with different name but same meaning Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 106 ++++++++++-------- .../patch/impl_test.go | 10 +- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index b8ad36a567d..76f562c969e 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -14,25 +14,15 @@ /* TODO: - - Detects the end of the existing envvars at the first line that does not declare an - envvar. This can lead to weird insertion positions if there is a comment in the - middle of the `env:` block. - - Tried performing a "dumber" implementation than the Python script, with less - "parsing" of the workflow. However, the location given by f.Offset isn't precise - enough. It only marks the start of the `run:` command, not the line where the - variable is actually used. Will therefore need to, at least, parse the `run` - command to replace all the instances of the unsafe variable. This means we can - have multiple identical remediations if the same variable is used multiple times - in the same step... that's just life. - Handle array inputs (i.e. workflow using `github.event.commits[0]` and `github.event.commits[1]`, which would duplicate $COMMIT_MESSAGE). - Handle use of synonyms (i.e `commits.*?\.author\.email` and - `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). + `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). Currently throws + an error on validation. - Handle cases where an existing envvar has the same name as what we'd use, but a different definition. - Handle cases where an existing envvar with a different name already covers our dangerous variable, but wasn't used. - - Move actionlint.Parse() to the calling function (once per file) */ package patch @@ -58,10 +48,6 @@ const ( assumedIndent = 2 ) -var ( - ymlLabelRegex = regexp.MustCompile(`^\s*[^#]:`) -) - // Fixes the script injection identified by the finding and returns a unified diff // users can apply (with `git apply` or `patch`) to fix the workflow themselves. // Should an error occur, it is handled and an empty patch is returned. @@ -84,17 +70,19 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow lines := strings.Split(string(content), "\n") - unsafePattern, ok := getUnsafePattern(unsafeVar) + existingEnvvars := parseExistingEnvvars(workflow) + envvarPatterns := buildUnsafePatterns() + useExistingEnvvars(envvarPatterns, existingEnvvars, unsafeVar) + + unsafePattern, ok := getUnsafePattern(unsafeVar, envvarPatterns) if !ok { // TODO: return meaningful error for logging, even if we don't throw it. return "", errors.New("AAA") } - envvars := parseExistingEnvvars(workflow) - lines = replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) - lines, ok = addEnvvarsToGlobalEnv(lines, envvars, unsafePattern, unsafeVar) + lines, ok = addEnvvarsToGlobalEnv(lines, existingEnvvars, unsafePattern, unsafeVar) if !ok { // TODO: return meaningful error for logging, even if we don't throw it. return "", errors.New("AAA") @@ -103,6 +91,18 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow return strings.Join(lines, "\n"), nil } +func useExistingEnvvars(unsafePatterns map[string]unsafePattern, existingEnvvars map[string]string, unsafeVar string) { + if envvar, ok := existingEnvvars[unsafeVar]; ok { + pattern, ok := getUnsafePattern(unsafeVar, unsafePatterns) + if !ok { + return + } + + pattern.envvarName = envvar + unsafePatterns[pattern.ghVarName] = pattern + } +} + // Adds a new global environment to a workflow. Assumes a global environment does not // yet exist. func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, bool) { @@ -198,6 +198,7 @@ func findNewEnvPos(lines []string, globalIndent int) (int, bool) { type unsafePattern struct { envvarName string + ghVarName string idRegex *regexp.Regexp replaceRegex *regexp.Regexp } @@ -205,12 +206,13 @@ type unsafePattern struct { func newUnsafePattern(e, p string) unsafePattern { return unsafePattern{ envvarName: e, + ghVarName: p, idRegex: regexp.MustCompile(p), replaceRegex: regexp.MustCompile(`{{\s*.*?` + p + `.*?\s*}}`), } } -func getUnsafePattern(unsafeVar string) (unsafePattern, bool) { +func getUnsafePattern(unsafeVar string, unsafePatterns map[string]unsafePattern) (unsafePattern, bool) { for _, p := range unsafePatterns { if p.idRegex.MatchString(unsafeVar) { p := p @@ -229,9 +231,9 @@ func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pa return lines, false } - if _, ok = existingEnvvars[unsafeVar]; ok { - // necessary envvar was already defined - return lines, ok + if _, ok := existingEnvvars[unsafeVar]; ok { + // an existing envvar already handles this unsafe var, we can simply use it + return lines, true } var insertPos, envvarIndent int @@ -301,29 +303,39 @@ func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex return lines } -var unsafePatterns = []unsafePattern{ - newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), - newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), - newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), - newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), - newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), - newUnsafePattern("COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), - newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), - newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), - newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), - newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), - newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), - newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), - newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), - newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), - newUnsafePattern("PR_DEFAULT_BRANCH", `github\.event\.pull_request\.head\.repo\.default_branch`), - newUnsafePattern("PR_HEAD_LABEL", `github\.event\.pull_request\.head\.label`), - newUnsafePattern("PR_HEAD_REF", `github\.event\.pull_request\.head\.ref`), - newUnsafePattern("PR_TITLE", `github\.event\.pull_request\.title`), - newUnsafePattern("REVIEW_BODY", `github\.event\.review\.body`), - newUnsafePattern("REVIEW_COMMENT_BODY", `github\.event\.review_comment\.body`), - - newUnsafePattern("HEAD_REF", `github\.head_ref`), +func buildUnsafePatterns() map[string]unsafePattern { + unsafePatterns := []unsafePattern{ + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), + newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), + newUnsafePattern("COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), + newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), + newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), + newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), + newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), + newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), + newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), + newUnsafePattern("PR_DEFAULT_BRANCH", `github\.event\.pull_request\.head\.repo\.default_branch`), + newUnsafePattern("PR_HEAD_LABEL", `github\.event\.pull_request\.head\.label`), + newUnsafePattern("PR_HEAD_REF", `github\.event\.pull_request\.head\.ref`), + newUnsafePattern("PR_TITLE", `github\.event\.pull_request\.title`), + newUnsafePattern("REVIEW_BODY", `github\.event\.review\.body`), + newUnsafePattern("REVIEW_COMMENT_BODY", `github\.event\.review_comment\.body`), + + newUnsafePattern("HEAD_REF", `github\.head_ref`), + } + m := make(map[string]unsafePattern) + + for _, p := range unsafePatterns { + p := p + m[p.ghVarName] = p + } + + return m } // Returns the indentation of the given line. The indentation is all whitespace and diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index a7b73379486..e236777876a 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -98,12 +98,10 @@ func Test_patchWorkflow(t *testing.T) { name: "Reuse existent workflow level env var, if has the same name we'd give", filePath: "reuseWorkflowLevelEnvVars.yaml", }, - // Test currently failing because we don't look for existent env vars pointing to the same content. - // Once proper behavior is implemented, enable this test - // { - // name: "Reuse existent workflow level env var, if it DOES NOT have the same name we'd give", - // inputFilepath: "reuseEnvVarWithDiffName.yaml", - // }, + { + name: "Reuse existent workflow level env var, if it DOES NOT have the same name we'd give", + filePath: "reuseEnvVarWithDiffName.yaml", + }, // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed // by the ones in smaller scope. From 5c986e8d9410d78da5a276db6b705ff49984cafb Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Sat, 29 Jun 2024 01:15:40 +0000 Subject: [PATCH 14/31] Avoid conflicts with irrelevant but existing envvars Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 19 +++++++--- .../patch/impl_test.go | 36 ++++++++++--------- .../testdata/envVarNameAlreadyInUse.yaml | 35 ++++++++++++++++++ .../envVarNameAlreadyInUse_fixed.yaml | 36 +++++++++++++++++++ 4 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 76f562c969e..8bd3cdc12e5 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -19,10 +19,7 @@ TODO: - Handle use of synonyms (i.e `commits.*?\.author\.email` and `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). Currently throws an error on validation. - - Handle cases where an existing envvar has the same name as what we'd use, but a - different definition. - - Handle cases where an existing envvar with a different name already covers our - dangerous variable, but wasn't used. + - Don't assume an indent of 2, use whatever is used in `jobs` */ package patch @@ -93,6 +90,8 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow func useExistingEnvvars(unsafePatterns map[string]unsafePattern, existingEnvvars map[string]string, unsafeVar string) { if envvar, ok := existingEnvvars[unsafeVar]; ok { + // There already exists an envvar handling our unsafe variable. + // Use that envvar instead of creating a separate envvar with the same value. pattern, ok := getUnsafePattern(unsafeVar, unsafePatterns) if !ok { return @@ -100,6 +99,18 @@ func useExistingEnvvars(unsafePatterns map[string]unsafePattern, existingEnvvars pattern.envvarName = envvar unsafePatterns[pattern.ghVarName] = pattern + return + } + + // if there's an envvar with the same name as what we'd use, add a "_1" suffix to + // our envvar name to avoid conflicts. Clumsy but works, and should be rare. + for _, e := range existingEnvvars { + for k, p := range unsafePatterns { + if e == p.envvarName { + p.envvarName += "_1" + unsafePatterns[k] = p + } + } } } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index e236777876a..b61bc2e4d33 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -102,22 +102,26 @@ func Test_patchWorkflow(t *testing.T) { name: "Reuse existent workflow level env var, if it DOES NOT have the same name we'd give", filePath: "reuseEnvVarWithDiffName.yaml", }, - // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. - // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed - // by the ones in smaller scope. - // Once proper behavior is implemented, enable this test - // { - // name: "Reuse env var already existent on smaller scope, it converts case of same or different names", - // inputFilepath: "reuseEnvVarSmallerScope.yaml", - // }, - // Test currently failing due to lack of style awareness. Currently we always add a blank line after - // the env block. - // Once proper behavior is implemented, enable this test. - // { - // name: "Keep style if file doesn't use blank lines between blocks", - // inputFilepath: "noLineBreaksBetweenBlocks.yaml", - // expectedFilepath: "noLineBreaksBetweenBlocks_fixed.yaml", - // }, + { + name: "Avoid conflict with existing envvar with same name but different value", + filePath: "envVarNameAlreadyInUse.yaml", + }, + // // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. + // // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed + // // by the ones in smaller scope. + // // Once proper behavior is implemented, enable this test + // // { + // // name: "Reuse env var already existent on smaller scope, it converts case of same or different names", + // // inputFilepath: "reuseEnvVarSmallerScope.yaml", + // // }, + // // Test currently failing due to lack of style awareness. Currently we always add a blank line after + // // the env block. + // // Once proper behavior is implemented, enable this test. + // // { + // // name: "Keep style if file doesn't use blank lines between blocks", + // // inputFilepath: "noLineBreaksBetweenBlocks.yaml", + // // expectedFilepath: "noLineBreaksBetweenBlocks_fixed.yaml", + // // }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml new file mode 100644 index 00000000000..0c61b74dbf3 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml @@ -0,0 +1,35 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + # existing envvar with the same name as what we'd use forces us to append a suffix. + COMMIT_MESSAGE: "this is a commit message" + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check msg + run: | + msg="${{ github.event.commits[0].message }}" + if [[ ! $msg =~ ^.*:\ .*$ ]]; then + echo "Bad message " + exit 1 + fi \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml new file mode 100644 index 00000000000..4703fc35685 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml @@ -0,0 +1,36 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + # existing envvar with the same name as what we'd use forces us to append a suffix. + COMMIT_MESSAGE: "this is a commit message" + COMMIT_MESSAGE_1: ${{ github.event.commits[0].message }} + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check msg + run: | + msg="$COMMIT_MESSAGE_1" + if [[ ! $msg =~ ^.*:\ .*$ ]]; then + echo "Bad message " + exit 1 + fi \ No newline at end of file From 65341552e47ff9547e9a1b56eab0d3935aff4d0b Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Sat, 29 Jun 2024 01:41:51 +0000 Subject: [PATCH 15/31] Use first job's indent to define envvar indent Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 33 +++++++++++++++---- .../crazyButValidIndentation_fixed.yaml | 2 +- .../patch/testdata/realExample1.diff | 23 ------------- 3 files changed, 27 insertions(+), 31 deletions(-) delete mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 8bd3cdc12e5..0e1760f7363 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -15,11 +15,11 @@ /* TODO: - Handle array inputs (i.e. workflow using `github.event.commits[0]` and - `github.event.commits[1]`, which would duplicate $COMMIT_MESSAGE). + `github.event.commits[1]`, which would duplicate $COMMIT_MESSAGE). Currently throws + an error on validation. - Handle use of synonyms (i.e `commits.*?\.author\.email` and `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). Currently throws an error on validation. - - Don't assume an indent of 2, use whatever is used in `jobs` */ package patch @@ -41,10 +41,6 @@ import ( "github.com/go-git/go-git/v5/storage/memory" ) -const ( - assumedIndent = 2 -) - // Fixes the script injection identified by the finding and returns a unified diff // users can apply (with `git apply` or `patch`) to fix the workflow themselves. // Should an error occur, it is handled and an empty patch is returned. @@ -258,7 +254,7 @@ func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pa // position now points to `env:`, insert variables below it insertPos += 1 - envvarIndent = globalIndentation + assumedIndent + envvarIndent = globalIndentation + getDefaultIndent(lines) } envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) @@ -380,6 +376,29 @@ func isParentLevelIndent(line string, parentIndent int) bool { return getIndent(line) <= parentIndent } +func getDefaultIndent(lines []string) int { + jobs := regexp.MustCompile(`^\s*jobs:`) + var jobsIndex, jobsIndent int + for i, line := range lines { + if jobs.MatchString(line) { + jobsIndex = i + jobsIndent = getIndent(line) + break + } + } + + jobIndent := jobsIndent + 2 // default value, should never be used + for _, line := range lines[jobsIndex+1:] { + if isBlankOrComment(line) { + continue + } + jobIndent = getIndent(line) + break + } + + return jobIndent - jobsIndent +} + func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) []*actionlint.Error { _, patchedErrs := actionlint.Parse([]byte(content)) if len(patchedErrs) == 0 { diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml index 2b49a1ed235..fd69558e3e2 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml @@ -14,7 +14,7 @@ on: [pull_request] env: - ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_TITLE: ${{ github.event.issue.title }} jobs: build: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff deleted file mode 100644 index 1a5e1d664c9..00000000000 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.diff +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/testdata/realExample1.yaml b/testdata/realExample1_fixed.yaml -index 843d0c71..cced3454 100644 ---- a/testdata/realExample1.yaml -+++ b/testdata/realExample1_fixed.yaml -@@ -6,6 +6,9 @@ on: - - permissions: read-all - -+env: -+ COMMENT_BODY: ${{ github.event.comment.body }} -+ - jobs: - benchmark-compare: - runs-on: ubuntu-latest -@@ -39,7 +42,7 @@ jobs: - - - name: Preparing benchmark for GitHub action - id: info -- run: yarn benchmarks prepare-for-github-action "${{github.event.comment.body}}" -+ run: yarn benchmarks prepare-for-github-action "$COMMENT_BODY" - - - run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}} - id: benchmark From bf2612095ef5d8bd69a8b7382b41d136bfcbd839 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Wed, 3 Jul 2024 23:39:31 +0000 Subject: [PATCH 16/31] Refactor patch/impl_test - Create helper function `readWorkflow` - Improved error handling in case of failed workflow validation - Allow the declaration of duplicate findings (cases where 2+ findings have the same patch) Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl_test.go | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index b61bc2e4d33..8b8e25a214b 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -29,6 +29,7 @@ import ( "github.com/rhysd/actionlint" "github.com/ossf/scorecard/v5/checker" + "github.com/ossf/scorecard/v5/checks/fileparser" "github.com/ossf/scorecard/v5/checks/raw" mockrepo "github.com/ossf/scorecard/v5/clients/mockclients" ) @@ -40,9 +41,9 @@ const ( func Test_patchWorkflow(t *testing.T) { t.Parallel() tests := []struct { + duplicates map[int]int // mark findings as duplicates of others, use same fix name string filePath string - duplicates map[int]int // mark findings as duplicates of others, use same fix }{ { // Extracted from real Angular fix: https://github.com/angular/angular/pull/51026/files @@ -128,71 +129,75 @@ func Test_patchWorkflow(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - dws := detectDangerousWorkflows(tt.filePath, t) + dws := detectDangerousWorkflows(t, tt.filePath) - inputContent, err := os.ReadFile(path.Join(testDir, tt.filePath)) + inputContent, workflow, inputErrs, err := readWorkflow(tt.filePath) if err != nil { - t.Errorf("Couldn't read input test file. Error:\n%s", err) - } - - workflow, inputErrs := actionlint.Parse(inputContent) - if len(inputErrs) > 0 && workflow == nil { - t.Errorf("Couldn't parse file as workflow. Error:\n%s", inputErrs[0]) + t.Errorf("Error reading workflow: %s", err) } numFindings := len(dws) for i, dw := range dws { - i = i + 1 // Only used for error messages, increment for legibility - - if dw.Type == checker.DangerousWorkflowUntrustedCheckout { - t.Errorf("Patching of untrusted checkout (finding #%d) is not implemented.", i) - } + i++ // Only used for error messages, increment for legibility output, err := patchWorkflow(dw.File, string(inputContent), workflow) if err != nil { - t.Errorf("Couldn't patch workflow for finding #%d.", i) + t.Errorf("Couldn't patch workflow for finding #%d. Error:\n%s", i, err) } patchedErrs := validatePatchedWorkflow(output, inputErrs) if len(patchedErrs) > 0 { - t.Errorf("Patched workflow for finding #%d is invalid. Error:\n%s", i, patchedErrs[0]) + t.Errorf("Patched workflow for finding #%d is invalid. Error:\n%s", i, + fileparser.FormatActionlintError(patchedErrs)) } if dup, ok := tt.duplicates[i]; ok { i = dup } - expected, err := getExpected(tt.filePath, numFindings, i) - if err != nil { - t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", i, err) - } + expected := getExpected(t, tt.filePath, numFindings, i) if diff := cmp.Diff(expected, output); diff != "" { t.Errorf("mismatch for finding #%d. (-want +got):\n%s", i, diff) } } - }) } } -func getExpected(filePath string, numFindings, findingIndex int) (string, error) { +func readWorkflow(filePath string) ([]byte, *actionlint.Workflow, []*actionlint.Error, error) { + inputContent, err := os.ReadFile(path.Join(testDir, filePath)) + if err != nil { + return nil, nil, nil, err + } + + workflow, inputErrs := actionlint.Parse(inputContent) + if len(inputErrs) > 0 && workflow == nil { + return inputContent, nil, inputErrs, inputErrs[0] + } + + return inputContent, workflow, inputErrs, nil +} + +func getExpected(t *testing.T, filePath string, numFindings, findingIndex int) string { + t.Helper() // build path to fixed version dot := strings.LastIndex(filePath, ".") fixedPath := filePath[:dot] + "_fixed" if numFindings > 1 { fixedPath = fmt.Sprintf("%s_%d", fixedPath, findingIndex) } - fixedPath = fixedPath + filePath[dot:] + fixedPath += filePath[dot:] content, err := os.ReadFile(path.Join(testDir, fixedPath)) if err != nil { - return "", err + t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", findingIndex, err) } - return string(content), nil + return string(content) } -func detectDangerousWorkflows(filePath string, t *testing.T) []checker.DangerousWorkflow { +func detectDangerousWorkflows(t *testing.T, filePath string) []checker.DangerousWorkflow { + t.Helper() ctrl := gomock.NewController(t) mockRepoClient := mockrepo.NewMockRepoClient(ctrl) mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return( @@ -210,7 +215,6 @@ func detectDangerousWorkflows(filePath string, t *testing.T) []checker.Dangerous } dw, err := raw.DangerousWorkflow(req) - if err != nil { t.Errorf("Error running raw.DangerousWorkflow. Error:\n%s", err) } From 31ea0545f9d928b173179df4a84700d7bf64d8d1 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 4 Jul 2024 13:52:38 +0000 Subject: [PATCH 17/31] patch/impl: Simplify unsafePatterns, use errors, docs, lint - Simplify use of unsafePatterns - Replaced boolean returns with errors, for easier log/debugging - Improved documentation - Changes to satisfy linter, adoption of 120-char line limit Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 3 +- .../patch/impl.go | 311 +++++++++--------- 2 files changed, 161 insertions(+), 153 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 527afc553b8..4fb639447b7 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -21,13 +21,14 @@ import ( "os" "path" + "github.com/rhysd/actionlint" + "github.com/ossf/scorecard/v5/checker" "github.com/ossf/scorecard/v5/finding" "github.com/ossf/scorecard/v5/internal/checknames" "github.com/ossf/scorecard/v5/internal/probes" "github.com/ossf/scorecard/v5/probes/hasDangerousWorkflowScriptInjection/patch" "github.com/ossf/scorecard/v5/probes/internal/utils/uerror" - "github.com/rhysd/actionlint" ) func init() { diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 0e1760f7363..d0dc9ccba61 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -14,37 +14,38 @@ /* TODO: - - Handle array inputs (i.e. workflow using `github.event.commits[0]` and - `github.event.commits[1]`, which would duplicate $COMMIT_MESSAGE). Currently throws - an error on validation. - - Handle use of synonyms (i.e `commits.*?\.author\.email` and - `head_commit\.author\.email“, which would duplicate $AUTHOR_EMAIL). Currently throws - an error on validation. + - Handle array inputs (i.e. workflow using `github.event.commits[0]` and `github.event.commits[1]`, which would + duplicate $COMMIT_MESSAGE). Currently throws an error on validation. + - Handle use of synonyms (i.e `commits.*?\.author\.email` and `head_commit\.author\.email“, which would duplicate + $AUTHOR_EMAIL). Currently throws an error on validation. */ package patch import ( - "errors" "fmt" "regexp" "slices" "strings" - "github.com/rhysd/actionlint" - - "github.com/ossf/scorecard/v5/checker" - "github.com/ossf/scorecard/v5/checks/fileparser" - "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" + "github.com/rhysd/actionlint" + + "github.com/ossf/scorecard/v5/checker" + "github.com/ossf/scorecard/v5/checks/fileparser" + sce "github.com/ossf/scorecard/v5/errors" ) -// Fixes the script injection identified by the finding and returns a unified diff -// users can apply (with `git apply` or `patch`) to fix the workflow themselves. -// Should an error occur, it is handled and an empty patch is returned. -func GeneratePatch(f checker.File, content string, workflow *actionlint.Workflow, workflowErrs []*actionlint.Error) (string, error) { +// Fixes the script injection identified by the finding and returns a unified diff users can apply (with `git apply` or +// `patch`) to fix the workflow themselves. Should an error occur, an empty patch is returned. +func GeneratePatch( + f checker.File, + content string, + workflow *actionlint.Workflow, + workflowErrs []*actionlint.Error, +) (string, error) { patchedWorkflow, err := patchWorkflow(f, content, workflow) if err != nil { return "", err @@ -61,113 +62,115 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow unsafeVar := strings.Trim(f.Snippet, " ") runCmdIndex := f.Offset - 1 - lines := strings.Split(string(content), "\n") + lines := strings.Split(content, "\n") existingEnvvars := parseExistingEnvvars(workflow) - envvarPatterns := buildUnsafePatterns() - useExistingEnvvars(envvarPatterns, existingEnvvars, unsafeVar) - - unsafePattern, ok := getUnsafePattern(unsafeVar, envvarPatterns) - if !ok { - // TODO: return meaningful error for logging, even if we don't throw it. - return "", errors.New("AAA") + unsafePattern, err := getUnsafePattern(unsafeVar) + if err != nil { + return "", err + } + unsafePattern, err = useExistingEnvvars(unsafePattern, existingEnvvars, unsafeVar) + if err != nil { + return "", err } - lines = replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) + replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) - lines, ok = addEnvvarsToGlobalEnv(lines, existingEnvvars, unsafePattern, unsafeVar) - if !ok { - // TODO: return meaningful error for logging, even if we don't throw it. - return "", errors.New("AAA") + lines, err = addEnvvarToGlobalEnv(lines, existingEnvvars, unsafePattern, unsafeVar) + if err != nil { + return "", sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("Unknown dangerous variable: %s", unsafeVar)) } return strings.Join(lines, "\n"), nil } -func useExistingEnvvars(unsafePatterns map[string]unsafePattern, existingEnvvars map[string]string, unsafeVar string) { +// Identifies whether the original workflow contains envvars which may conflict with our patch. +// Should an existing envvar already handle our dangerous variable, it will be used in the patch instead of creating a +// new envvar with the same value. +// Should an existing envvar have the same name as the one that would ordinarily be used by the patch, the patch appends +// a suffix to the patch's envvar name to avoid conflicts. +// +// Returns the unsafePattern, possibly updated to consider the existing envvars. +func useExistingEnvvars( + pattern unsafePattern, + existingEnvvars map[string]string, + unsafeVar string, +) (unsafePattern, error) { if envvar, ok := existingEnvvars[unsafeVar]; ok { // There already exists an envvar handling our unsafe variable. - // Use that envvar instead of creating a separate envvar with the same value. - pattern, ok := getUnsafePattern(unsafeVar, unsafePatterns) - if !ok { - return - } - + // Use that envvar instead of creating another one with the same value. pattern.envvarName = envvar - unsafePatterns[pattern.ghVarName] = pattern - return + return pattern, nil } - // if there's an envvar with the same name as what we'd use, add a "_1" suffix to - // our envvar name to avoid conflicts. Clumsy but works, and should be rare. + // If there's an envvar with the same name as what we'd use, add a hard-coded suffix to our name to avoid conflicts. + // Clumsy but works in almost all cases, and should be rare. for _, e := range existingEnvvars { - for k, p := range unsafePatterns { - if e == p.envvarName { - p.envvarName += "_1" - unsafePatterns[k] = p - } + if e == pattern.envvarName { + pattern.envvarName += "_1" + return pattern, nil } } -} -// Adds a new global environment to a workflow. Assumes a global environment does not -// yet exist. -func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, bool) { - envPos, ok := findNewEnvPos(lines, globalIndentation) + return pattern, nil +} - if !ok { - // invalid workflow, could not determine location for new environment - return nil, envPos, ok +// Adds a new global environment followed by a blank line to a workflow. +// Assumes a global environment does not yet exist. +// +// Returns: +// - []string: the new array of lines describing the workflow, now with the global `env:` inserted. +// - int: the row where the `env:` block was added +func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, error) { + envPos, err := findNewEnvPos(lines, globalIndentation) + if err != nil { + return nil, -1, err } label := strings.Repeat(" ", globalIndentation) + "env:" lines = slices.Insert(lines, envPos, []string{label, ""}...) - return lines, envPos, ok + return lines, envPos, nil } -// Identifies the "global" indentation, as defined by the indentation on the required -// `on:` block. Will equal 0 in almost all cases. -func findGlobalIndentation(lines []string) (int, bool) { +// Returns the "global" indentation, as defined by the indentation on the required `on:` block. +// Will equal 0 in almost all cases. +func findGlobalIndentation(lines []string) (int, error) { r := regexp.MustCompile(`^\s*on:`) for _, line := range lines { if r.MatchString(line) { - return getIndent(line), true + return getIndent(line), nil } } - return -1, false + return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine global indentation") } -// Detects whether a global `env:` block already exists. +// Detects where the existing global `env:` block is located. // // Returns: -// - int: the index for the line where the `env:` block is declared +// - int: the index for the line where a new global envvar should be added (after the last existing envvar) // - int: the indentation used for the declared environment variables // -// The first two values return -1 if the `env` block doesn't exist +// Both values return -1 if the `env` block doesn't exist or is invalid. func findExistingEnv(lines []string, globalIndent int) (int, int) { - num_lines := len(lines) - indent := strings.Repeat(" ", globalIndent) - - // regex to detect the global `env:` block - labelRegex := regexp.MustCompile(indent + "env:") - var currPos int var line string + envRegex := labelRegex("env", globalIndent) for currPos, line = range lines { - if labelRegex.MatchString(line) { + if envRegex.MatchString(line) { break } } - if currPos >= num_lines-1 { - // there must be at least one more line + if currPos >= len(lines)-1 { + // Invalid env, there must be at least one more line for an existing envvar. Shouldn't happen. return -1, -1 } currPos++ // move to line after `env:` - insertPos := currPos // mark the position where new envvars will be added - envvarIndent := getIndent(lines[currPos]) + insertPos := currPos // marks the position where new envvars will be added + var envvarIndent int for i, line := range lines[currPos:] { if isBlankOrComment(line) { continue @@ -178,36 +181,30 @@ func findExistingEnv(lines []string, globalIndent int) (int, int) { break } + envvarIndent = getIndent(line) insertPos = currPos + i + 1 } return insertPos, envvarIndent } -// Identifies the line where a new `env:` block should be inserted: right above the -// `jobs:` label. -// -// Returns: -// - int: the index for the line where the `env:` block should be inserted -// - bool: whether the `jobs:` block was found. Should always be `true` -func findNewEnvPos(lines []string, globalIndent int) (int, bool) { - // the new env is added right before `jobs:` - indent := strings.Repeat(" ", globalIndent) - r := regexp.MustCompile(indent + "jobs:") +// Returns the line where a new `env:` block should be inserted: right above the `jobs:` label. +func findNewEnvPos(lines []string, globalIndent int) (int, error) { + jobsRegex := labelRegex("jobs", globalIndent) for i, line := range lines { - if r.MatchString(line) { - return i, true + if jobsRegex.MatchString(line) { + return i, nil } } - return -1, false + return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine location for new environment") } type unsafePattern struct { - envvarName string - ghVarName string idRegex *regexp.Regexp replaceRegex *regexp.Regexp + envvarName string + ghVarName string } func newUnsafePattern(e, p string) unsafePattern { @@ -219,52 +216,47 @@ func newUnsafePattern(e, p string) unsafePattern { } } -func getUnsafePattern(unsafeVar string, unsafePatterns map[string]unsafePattern) (unsafePattern, bool) { - for _, p := range unsafePatterns { - if p.idRegex.MatchString(unsafeVar) { - p := p - return p, true - } - } - return unsafePattern{}, false -} - -// Adds the necessary environment variable to the global `env:` block, if it exists. -// If the `env:` block does not exist, it is created right above the `jobs:` label. -func addEnvvarsToGlobalEnv(lines []string, existingEnvvars map[string]string, pattern unsafePattern, unsafeVar string) ([]string, bool) { - globalIndentation, ok := findGlobalIndentation(lines) - if !ok { - // invalid workflow, could not determine global indentation - return lines, false +// Adds the necessary environment variable to the global `env:` block. +// If the `env:` block does not exist, it is created right above the `jobs:` block. +// +// Returns the new array of lines describing the workflow after inserting the new envvar. +func addEnvvarToGlobalEnv( + lines []string, + existingEnvvars map[string]string, + pattern unsafePattern, unsafeVar string, +) ([]string, error) { + globalIndentation, err := findGlobalIndentation(lines) + if err != nil { + return lines, err } if _, ok := existingEnvvars[unsafeVar]; ok { // an existing envvar already handles this unsafe var, we can simply use it - return lines, true + return lines, nil } var insertPos, envvarIndent int if len(existingEnvvars) > 0 { insertPos, envvarIndent = findExistingEnv(lines, globalIndentation) } else { - lines, insertPos, ok = addNewGlobalEnv(lines, globalIndentation) - if !ok { - return lines, ok + lines, insertPos, err = addNewGlobalEnv(lines, globalIndentation) + if err != nil { + return lines, err } // position now points to `env:`, insert variables below it - insertPos += 1 - envvarIndent = globalIndentation + getDefaultIndent(lines) + insertPos++ + envvarIndent = globalIndentation + getDefaultIndentStep(lines) } envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) - lines = slices.Insert(lines, insertPos, - strings.Repeat(" ", envvarIndent)+envvarDefinition, - ) + lines = slices.Insert(lines, insertPos, strings.Repeat(" ", envvarIndent)+envvarDefinition) - return lines, true + return lines, nil } +// Parses the envvars from the existing global `env:` block. +// Returns a map from the GitHub variable name to the envvar name (i.e. "github.event.issue.body": "ISSUE_BODY"). func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { envvars := make(map[string]string) @@ -293,24 +285,20 @@ func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { return envvars } -// Replaces all instances of the given script injection variable with the safe -// environment variable. -func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) []string { +// Replaces all instances of the given script injection variable with the safe environment variable. +func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) { runIndent := getIndent(lines[runIndex]) for i, line := range lines[runIndex:] { currLine := int(runIndex) + i if i > 0 && isParentLevelIndent(lines[currLine], runIndent) { - // anything at the same indent as the first line of the `- run:` block will - // mean the end of the run block. + // anything at the same indent as the first line of the `- run:` block will mean the end of the run block. break } lines[currLine] = pattern.replaceRegex.ReplaceAllString(line, pattern.envvarName) } - - return lines } -func buildUnsafePatterns() map[string]unsafePattern { +func getUnsafePattern(unsafeVar string) (unsafePattern, error) { unsafePatterns := []unsafePattern{ newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), @@ -335,22 +323,23 @@ func buildUnsafePatterns() map[string]unsafePattern { newUnsafePattern("HEAD_REF", `github\.head_ref`), } - m := make(map[string]unsafePattern) - for _, p := range unsafePatterns { p := p - m[p.ghVarName] = p + if p.idRegex.MatchString(unsafeVar) { + return p, nil + } } - return m + return unsafePattern{}, sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("Unknown dangerous variable: %s", unsafeVar)) } -// Returns the indentation of the given line. The indentation is all whitespace and -// dashes before a key or value. +// Returns the indentation of the given line. The indentation is all leading whitespace and dashes. func getIndent(line string) int { return len(line) - len(strings.TrimLeft(line, " -")) } +// Returns whether the given line is a blank line or only contains comments. func isBlankOrComment(line string) bool { blank := regexp.MustCompile(`^\s*$`) comment := regexp.MustCompile(`^\s*#`) @@ -368,7 +357,7 @@ func isBlankOrComment(line string) bool { // ... # they all have indent = 4 // job_bar: # this line has job_foo's indentation, so we know job_foo is done // -// Blank lines and those containing only comments are ignored and always return False. +// Blank lines and those containing only comments are ignored and always return false. func isParentLevelIndent(line string, parentIndent int) bool { if isBlankOrComment(line) { return false @@ -376,7 +365,13 @@ func isParentLevelIndent(line string, parentIndent int) bool { return getIndent(line) <= parentIndent } -func getDefaultIndent(lines []string) int { +func labelRegex(label string, indent int) *regexp.Regexp { + return regexp.MustCompile(fmt.Sprintf("^%s%s:", strings.Repeat(" ", indent), label)) +} + +// Returns the default indentation step adopted in the document. +// This is taken from the difference in indentation between the `jobs:` label and the first job's label. +func getDefaultIndentStep(lines []string) int { jobs := regexp.MustCompile(`^\s*jobs:`) var jobsIndex, jobsIndent int for i, line := range lines { @@ -399,6 +394,10 @@ func getDefaultIndent(lines []string) int { return jobIndent - jobsIndent } +// Validates that the patch does not add any new syntax errors to the workflow. If the original workflow contains +// errors, then the patched version also might. As long as all the patch's errors match the original's, it is validated. +// +// Returns the array of new parsing errors caused by the patch. func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) []*actionlint.Error { _, patchedErrs := actionlint.Parse([]byte(content)) if len(patchedErrs) == 0 { @@ -409,8 +408,8 @@ func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) [ } normalizeMsg := func(msg string) string { - // one of the error messages contains line metadata that may legitimately change - // after a patch. Only looking at the errors' first sentence eliminates this. + // one of the error messages contains line metadata that may legitimately change after a patch. + // Only looking at the errors' first sentence eliminates this. return strings.Split(msg, ".")[0] } @@ -443,8 +442,7 @@ func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) [ return newErrs } -// Returns the changes between the original and patched workflows as a unified diff -// (the same generated by `git diff` or `diff -u`). +// Returns the changes between the original and patched workflows as a unified diff (same as `git diff` or `diff -u`). func getDiff(path, original, patched string) (string, error) { // initialize an in-memory repository repo, err := newInMemoryRepo() @@ -452,72 +450,81 @@ func getDiff(path, original, patched string) (string, error) { return "", err } - // commit original workflow to in-memory repository + // commit original workflow originalCommit, err := commitWorkflow(path, original, repo) if err != nil { return "", err } - // commit patched workflow to in-memory repository + // commit patched workflow patchedCommit, err := commitWorkflow(path, patched, repo) if err != nil { return "", err } + // get diff between those commits return toUnifiedDiff(originalCommit, patchedCommit) } -// Initializes an in-memory repository func newInMemoryRepo() (*git.Repository, error) { - // initialize an in-memory repository - filesystem := memfs.New() - repo, err := git.Init(memory.NewStorage(), filesystem) + repo, err := git.Init(memory.NewStorage(), memfs.New()) if err != nil { - return nil, err + return nil, fmt.Errorf("git.Init: %w", err) } return repo, nil } -// Commits the workflow at the given path to the in-memory repository +// Commits the workflow at the given path to the in-memory repository. func commitWorkflow(path, contents string, repo *git.Repository) (*object.Commit, error) { worktree, err := repo.Worktree() if err != nil { - return nil, err + return nil, fmt.Errorf("repo.Worktree: %w", err) } filesystem := worktree.Filesystem // create (or overwrite) file df, err := filesystem.Create(path) if err != nil { - return nil, err + return nil, fmt.Errorf("filesystem.Create: %w", err) } - df.Write([]byte(contents)) + _, err = df.Write([]byte(contents)) + if err != nil { + return nil, fmt.Errorf("df.Write: %w", err) + } df.Close() // commit file to in-memory repository - worktree.Add(path) + _, err = worktree.Add(path) + if err != nil { + return nil, fmt.Errorf("worktree.Add: %w", err) + } + hash, err := worktree.Commit("x", &git.CommitOptions{}) if err != nil { - return nil, err + return nil, fmt.Errorf("worktree.Commit: %w", err) } commit, err := repo.CommitObject(hash) if err != nil { - return nil, err + return nil, fmt.Errorf("repo.CommitObject: %w", err) } + return commit, nil } -// Returns a unified diff describing the difference between the given commits +// Returns a unified diff describing the difference between the given commits. func toUnifiedDiff(originalCommit, patchedCommit *object.Commit) (string, error) { patch, err := originalCommit.Patch(patchedCommit) if err != nil { - return "", err + return "", fmt.Errorf("originalCommit.Patch: %w", err) } builder := strings.Builder{} - patch.Encode(&builder) + err = patch.Encode(&builder) + if err != nil { + return "", fmt.Errorf("patch.Encode: %w", err) + } return builder.String(), nil } From e61d79a12670320f3b7de95b3a3e1655ee03e38f Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 4 Jul 2024 13:58:30 +0000 Subject: [PATCH 18/31] Fix panic in hasScriptInjection test due to missing file Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/impl_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/impl_test.go index 06a8651b5ff..d89adc8a90c 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl_test.go @@ -43,6 +43,9 @@ func Test_Run(t *testing.T) { Workflows: []checker.DangerousWorkflow{ { Type: checker.DangerousWorkflowScriptInjection, + File: checker.File{ + Path: "patch/testdata/userInputAssignedToVariable.yaml", + }, }, }, }, From bbe6c85c7b543207ed0373614bd9e3bcef493e31 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 4 Jul 2024 20:58:07 +0000 Subject: [PATCH 19/31] Avoid duplicate envvars dealing with array variables Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 34 +++++++++++-------- .../patch/impl_test.go | 4 +++ .../patch/testdata/arrayVariables.yaml | 27 +++++++++++++++ .../testdata/arrayVariables_fixed_1.yaml | 30 ++++++++++++++++ .../testdata/arrayVariables_fixed_2.yaml | 30 ++++++++++++++++ .../testdata/envVarNameAlreadyInUse.yaml | 2 +- .../envVarNameAlreadyInUse_fixed.yaml | 2 +- .../testdata/reuseWorkflowLevelEnvVars.yaml | 2 +- .../reuseWorkflowLevelEnvVars_fixed_1.yaml | 4 +-- .../reuseWorkflowLevelEnvVars_fixed_2.yaml | 2 +- .../reuseWorkflowLevelEnvVars_fixed_3.yaml | 2 +- .../userInputAssignedToVariable_fixed.yaml | 4 +-- 12 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml create mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index d0dc9ccba61..3d515e0944c 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -4,21 +4,13 @@ // 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 +// 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. - -/* -TODO: - - Handle array inputs (i.e. workflow using `github.event.commits[0]` and `github.event.commits[1]`, which would - duplicate $COMMIT_MESSAGE). Currently throws an error on validation. - - Handle use of synonyms (i.e `commits.*?\.author\.email` and `head_commit\.author\.email“, which would duplicate - $AUTHOR_EMAIL). Currently throws an error on validation. -*/ package patch import ( @@ -209,9 +201,13 @@ type unsafePattern struct { func newUnsafePattern(e, p string) unsafePattern { return unsafePattern{ - envvarName: e, - ghVarName: p, - idRegex: regexp.MustCompile(p), + envvarName: e, + ghVarName: p, + // Regex to simply identify the unsafe variable that triggered the finding. + // Must use a regex and not a simple string to identify possible uses of array variables + // (i.e. `github.event.commits[0].author.email`). + idRegex: regexp.MustCompile(p), + // Regex to replace the unsafe variable in a `run` command with the envvar name. replaceRegex: regexp.MustCompile(`{{\s*.*?` + p + `.*?\s*}}`), } } @@ -305,12 +301,12 @@ func getUnsafePattern(unsafeVar string) (unsafePattern, error) { newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), - newUnsafePattern("COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), + newUnsafePattern("ISSUE_COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), @@ -323,10 +319,20 @@ func getUnsafePattern(unsafeVar string) (unsafePattern, error) { newUnsafePattern("HEAD_REF", `github\.head_ref`), } + for _, p := range unsafePatterns { p := p if p.idRegex.MatchString(unsafeVar) { - return p, nil + arrayVarRegex := regexp.MustCompile(`\[(.+?)\]`) + arrayIdx := arrayVarRegex.FindStringSubmatch(unsafeVar) + if arrayIdx == nil || len(arrayIdx) < 2 { + // not an array variable, the default envvar name is sufficient. + return p, nil + } + // Array variable (i.e. `github.event.commits[0].message`), must avoid potential conflicts. + // Add the array index to the name as a suffix, and use the exact unsafe variable name instead of the + // default, which includes a regex that will catch all instances of the array. + return newUnsafePattern(p.envvarName+"_"+arrayIdx[1], regexp.QuoteMeta(unsafeVar)), nil } } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 8b8e25a214b..12f93976152 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -107,6 +107,10 @@ func Test_patchWorkflow(t *testing.T) { name: "Avoid conflict with existing envvar with same name but different value", filePath: "envVarNameAlreadyInUse.yaml", }, + { + name: "Avoid conflict between array variables", + filePath: "arrayVariables.yaml", + }, // // Test currently failing because we don't look for existent env vars on smaller scopes -- job-level or step-level. // // In this case, we're always creating a new workflow-level env var. Note that this could lead to creation of env vars shadowed // // by the ones in smaller scope. diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml new file mode 100644 index 00000000000..7b58f8e64be --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - run: | + echo "${{ github.event.commits[0].message }}" + echo "${{ github.event.commits[1].message }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml new file mode 100644 index 00000000000..c64c5d3f50b --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + COMMIT_MESSAGE_0: ${{ github.event.commits[0].message }} + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - run: | + echo "$COMMIT_MESSAGE_0" + echo "${{ github.event.commits[1].message }}" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml new file mode 100644 index 00000000000..d0b3e17a994 --- /dev/null +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 OpenSSF 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. +on: [pull_request] + +env: + COMMIT_MESSAGE_1: ${{ github.event.commits[1].message }} + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - run: | + echo "${{ github.event.commits[0].message }}" + echo "$COMMIT_MESSAGE_1" \ No newline at end of file diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml index 0c61b74dbf3..0d8d28a28da 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml @@ -28,7 +28,7 @@ jobs: - name: Check msg run: | - msg="${{ github.event.commits[0].message }}" + msg="${{ github.event.head_commit.message }}" if [[ ! $msg =~ ^.*:\ .*$ ]]; then echo "Bad message " exit 1 diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml index 4703fc35685..78f338477c1 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml @@ -16,7 +16,7 @@ on: [pull_request] env: # existing envvar with the same name as what we'd use forces us to append a suffix. COMMIT_MESSAGE: "this is a commit message" - COMMIT_MESSAGE_1: ${{ github.event.commits[0].message }} + COMMIT_MESSAGE_1: ${{ github.event.head_commit.message }} jobs: build: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml index bc035386161..0b5a6dddb51 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml @@ -19,7 +19,7 @@ env: ISSUE_NUMBER: ${{github.event.issue.number}} # Safe but unnused env var. Same name that our script would use. No spaces inside brackets - COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" on: issue_comment: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml index f449c9dcd6f..71a2e7f783d 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml @@ -19,7 +19,7 @@ env: ISSUE_NUMBER: ${{github.event.issue.number}} # Safe but unnused env var. Same name that our script would use. No spaces inside brackets - COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" on: issue_comment: @@ -36,7 +36,7 @@ jobs: steps: - run: | echo "$ISSUE_NUMBER" - echo "$COMMENT_BODY" + echo "$ISSUE_COMMENT_BODY" # content orinally not present in any env var. # This same content will be used again and should reuse created env var diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml index de202d49027..ffc3aedec57 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml @@ -19,7 +19,7 @@ env: ISSUE_NUMBER: ${{github.event.issue.number}} # Safe but unnused env var. Same name that our script would use. No spaces inside brackets - COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" ISSUE_BODY: ${{ github.event.issue.body }} on: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml index 369170760cd..e0dc33f2b80 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml @@ -19,7 +19,7 @@ env: ISSUE_NUMBER: ${{github.event.issue.number}} # Safe but unnused env var. Same name that our script would use. No spaces inside brackets - COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" + ISSUE_COMMENT_BODY: "${{github.event.issue_comment.comment.body}}" ISSUE_BODY: ${{ github.event.issue.body }} on: diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml index cc9069f7a5f..c016472bb19 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml @@ -14,7 +14,7 @@ on: [pull_request] env: - COMMIT_MESSAGE: ${{ github.event.commits[0].message }} + COMMIT_MESSAGE_0: ${{ github.event.commits[0].message }} jobs: build: @@ -27,7 +27,7 @@ jobs: - name: Check msg run: | - msg="$COMMIT_MESSAGE" + msg="$COMMIT_MESSAGE_0" if [[ ! $msg =~ ^.*:\ .*$ ]]; then echo "Bad message " exit 1 From 09d4b47f0d2fec27cf66b51a75fbe9ca407b82d9 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 4 Jul 2024 21:29:59 +0000 Subject: [PATCH 20/31] Adopt existing inter-block spacing for new env Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 51 +++++++++++++++++-- .../testdata/crazyButValidIndentation.yaml | 1 + .../crazyButValidIndentation_fixed.yaml | 2 + 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 3d515e0944c..505aef79a62 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -121,7 +121,14 @@ func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, erro } label := strings.Repeat(" ", globalIndentation) + "env:" - lines = slices.Insert(lines, envPos, []string{label, ""}...) + content := []string{label} + + numBlankLines := getDefaultBlockSpacing(lines, globalIndentation) + for i := 0; i < numBlankLines; i++ { + content = append(content, "") + } + + lines = slices.Insert(lines, envPos, content...) return lines, envPos, nil } @@ -345,12 +352,48 @@ func getIndent(line string) int { return len(line) - len(strings.TrimLeft(line, " -")) } -// Returns whether the given line is a blank line or only contains comments. -func isBlankOrComment(line string) bool { +// Returns the "default" number of blank lines between blocks. +// The default is taken as the number of blank lines between the `jobs` label and the end of the preceding block. +func getDefaultBlockSpacing(lines []string, globalIndent int) int { + jobsRegex := labelRegex("jobs", globalIndent) + + var jobsIdx int + var line string + for jobsIdx, line = range lines { + if jobsRegex.MatchString(line) { + break + } + } + + numBlanks := 0 + for i := jobsIdx - 1; i >= 0; i-- { + line := lines[i] + + if isBlank(line) { + numBlanks++ + } else if !isComment(line) { + // If the line is neither blank nor a comment, then we've reached the end of the previous block. + break + } + } + + return numBlanks +} + +// Returns whether the given line is a blank line (empty or only whitespace). +func isBlank(line string) bool { blank := regexp.MustCompile(`^\s*$`) + return blank.MatchString(line) +} + +// Returns whether the given line only contains comments. +func isComment(line string) bool { comment := regexp.MustCompile(`^\s*#`) + return comment.MatchString(line) +} - return blank.MatchString(line) || comment.MatchString(line) +func isBlankOrComment(line string) bool { + return isBlank(line) || isComment(line) } // Returns whether the given line is at the same indentation level as the parent scope. diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml index 0131bbf1605..a3535b79e63 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml @@ -13,6 +13,7 @@ # limitations under the License. on: [pull_request] + jobs: build: runs-on: ubuntu-latest diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml index fd69558e3e2..f75bd1d05b4 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml +++ b/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml @@ -13,9 +13,11 @@ # limitations under the License. on: [pull_request] + env: ISSUE_TITLE: ${{ github.event.issue.title }} + jobs: build: runs-on: ubuntu-latest From 89b73a3e5451ae5ea7a9d34a269bd14ffa0a3627 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 4 Jul 2024 21:49:53 +0000 Subject: [PATCH 21/31] chore: Tidy up function order, remove unused files Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../patch/impl.go | 337 +++++++++--------- .../patch/parse_workflow.go | 15 - .../patch/parse_workflow_test.go | 15 - 3 files changed, 169 insertions(+), 198 deletions(-) delete mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go delete mode 100644 probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 505aef79a62..02093a03ef2 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -30,6 +30,13 @@ import ( sce "github.com/ossf/scorecard/v5/errors" ) +type unsafePattern struct { + idRegex *regexp.Regexp + replaceRegex *regexp.Regexp + envvarName string + ghVarName string +} + // Fixes the script injection identified by the finding and returns a unified diff users can apply (with `git apply` or // `patch`) to fix the workflow themselves. Should an error occur, an empty patch is returned. func GeneratePatch( @@ -56,11 +63,12 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow lines := strings.Split(content, "\n") - existingEnvvars := parseExistingEnvvars(workflow) unsafePattern, err := getUnsafePattern(unsafeVar) if err != nil { return "", err } + + existingEnvvars := parseExistingEnvvars(workflow) unsafePattern, err = useExistingEnvvars(unsafePattern, existingEnvvars, unsafeVar) if err != nil { return "", err @@ -77,6 +85,95 @@ func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow return strings.Join(lines, "\n"), nil } +func getUnsafePattern(unsafeVar string) (unsafePattern, error) { + unsafePatterns := []unsafePattern{ + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), + newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), + newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), + newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), + newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), + newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), + newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), + newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), + newUnsafePattern("ISSUE_COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), + newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), + newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), + newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), + newUnsafePattern("PR_DEFAULT_BRANCH", `github\.event\.pull_request\.head\.repo\.default_branch`), + newUnsafePattern("PR_HEAD_LABEL", `github\.event\.pull_request\.head\.label`), + newUnsafePattern("PR_HEAD_REF", `github\.event\.pull_request\.head\.ref`), + newUnsafePattern("PR_TITLE", `github\.event\.pull_request\.title`), + newUnsafePattern("REVIEW_BODY", `github\.event\.review\.body`), + newUnsafePattern("REVIEW_COMMENT_BODY", `github\.event\.review_comment\.body`), + + newUnsafePattern("HEAD_REF", `github\.head_ref`), + } + + for _, p := range unsafePatterns { + p := p + if p.idRegex.MatchString(unsafeVar) { + arrayVarRegex := regexp.MustCompile(`\[(.+?)\]`) + arrayIdx := arrayVarRegex.FindStringSubmatch(unsafeVar) + if arrayIdx == nil || len(arrayIdx) < 2 { + // not an array variable, the default envvar name is sufficient. + return p, nil + } + // Array variable (i.e. `github.event.commits[0].message`), must avoid potential conflicts. + // Add the array index to the name as a suffix, and use the exact unsafe variable name instead of the + // default, which includes a regex that will catch all instances of the array. + return newUnsafePattern(p.envvarName+"_"+arrayIdx[1], regexp.QuoteMeta(unsafeVar)), nil + } + } + + return unsafePattern{}, sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("Unknown dangerous variable: %s", unsafeVar)) +} + +func newUnsafePattern(e, p string) unsafePattern { + return unsafePattern{ + envvarName: e, + ghVarName: p, + // Regex to simply identify the unsafe variable that triggered the finding. + // Must use a regex and not a simple string to identify possible uses of array variables + // (i.e. `github.event.commits[0].author.email`). + idRegex: regexp.MustCompile(p), + // Regex to replace the unsafe variable in a `run` command with the envvar name. + replaceRegex: regexp.MustCompile(`{{\s*.*?` + p + `.*?\s*}}`), + } +} + +// Parses the envvars from the existing global `env:` block. +// Returns a map from the GitHub variable name to the envvar name (i.e. "github.event.issue.body": "ISSUE_BODY"). +func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { + envvars := make(map[string]string) + + if workflow.Env == nil { + return envvars + } + + r := regexp.MustCompile(`\$\{\{\s*(github\.[^\s]*?)\s*}}`) + for _, v := range workflow.Env.Vars { + value := v.Value.Value + + if strings.Contains(value, "${{") { + // extract simple variable definition (without brackets, etc) + m := r.FindStringSubmatch(value) + if len(m) == 2 { + value = m[1] + envvars[value] = v.Name.Value + } else { + envvars[v.Value.Value] = v.Name.Value + } + } else { + envvars[v.Value.Value] = v.Name.Value + } + } + + return envvars +} + // Identifies whether the original workflow contains envvars which may conflict with our patch. // Should an existing envvar already handle our dangerous variable, it will be used in the patch instead of creating a // new envvar with the same value. @@ -108,41 +205,56 @@ func useExistingEnvvars( return pattern, nil } -// Adds a new global environment followed by a blank line to a workflow. -// Assumes a global environment does not yet exist. +// Replaces all instances of the given script injection variable with the safe environment variable. +func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) { + runIndent := getIndent(lines[runIndex]) + for i, line := range lines[runIndex:] { + currLine := int(runIndex) + i + if i > 0 && isParentLevelIndent(lines[currLine], runIndent) { + // anything at the same indent as the first line of the `- run:` block will mean the end of the run block. + break + } + lines[currLine] = pattern.replaceRegex.ReplaceAllString(line, pattern.envvarName) + } +} + +// Adds the necessary environment variable to the global `env:` block. +// If the `env:` block does not exist, it is created right above the `jobs:` block. // -// Returns: -// - []string: the new array of lines describing the workflow, now with the global `env:` inserted. -// - int: the row where the `env:` block was added -func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, error) { - envPos, err := findNewEnvPos(lines, globalIndentation) +// Returns the new array of lines describing the workflow after inserting the new envvar. +func addEnvvarToGlobalEnv( + lines []string, + existingEnvvars map[string]string, + pattern unsafePattern, unsafeVar string, +) ([]string, error) { + globalIndentation, err := findGlobalIndentation(lines) if err != nil { - return nil, -1, err + return lines, err } - label := strings.Repeat(" ", globalIndentation) + "env:" - content := []string{label} - - numBlankLines := getDefaultBlockSpacing(lines, globalIndentation) - for i := 0; i < numBlankLines; i++ { - content = append(content, "") + if _, ok := existingEnvvars[unsafeVar]; ok { + // an existing envvar already handles this unsafe var, we can simply use it + return lines, nil } - lines = slices.Insert(lines, envPos, content...) - return lines, envPos, nil -} - -// Returns the "global" indentation, as defined by the indentation on the required `on:` block. -// Will equal 0 in almost all cases. -func findGlobalIndentation(lines []string) (int, error) { - r := regexp.MustCompile(`^\s*on:`) - for _, line := range lines { - if r.MatchString(line) { - return getIndent(line), nil + var insertPos, envvarIndent int + if len(existingEnvvars) > 0 { + insertPos, envvarIndent = findExistingEnv(lines, globalIndentation) + } else { + lines, insertPos, err = addNewGlobalEnv(lines, globalIndentation) + if err != nil { + return lines, err } + + // position now points to `env:`, insert variables below it + insertPos++ + envvarIndent = globalIndentation + getDefaultIndentStep(lines) } - return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine global indentation") + envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) + lines = slices.Insert(lines, insertPos, strings.Repeat(" ", envvarIndent)+envvarDefinition) + + return lines, nil } // Detects where the existing global `env:` block is located. @@ -187,164 +299,53 @@ func findExistingEnv(lines []string, globalIndent int) (int, int) { return insertPos, envvarIndent } -// Returns the line where a new `env:` block should be inserted: right above the `jobs:` label. -func findNewEnvPos(lines []string, globalIndent int) (int, error) { - jobsRegex := labelRegex("jobs", globalIndent) - for i, line := range lines { - if jobsRegex.MatchString(line) { - return i, nil - } - } - - return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine location for new environment") -} - -type unsafePattern struct { - idRegex *regexp.Regexp - replaceRegex *regexp.Regexp - envvarName string - ghVarName string -} - -func newUnsafePattern(e, p string) unsafePattern { - return unsafePattern{ - envvarName: e, - ghVarName: p, - // Regex to simply identify the unsafe variable that triggered the finding. - // Must use a regex and not a simple string to identify possible uses of array variables - // (i.e. `github.event.commits[0].author.email`). - idRegex: regexp.MustCompile(p), - // Regex to replace the unsafe variable in a `run` command with the envvar name. - replaceRegex: regexp.MustCompile(`{{\s*.*?` + p + `.*?\s*}}`), - } -} - -// Adds the necessary environment variable to the global `env:` block. -// If the `env:` block does not exist, it is created right above the `jobs:` block. +// Adds a new global environment followed by a blank line to a workflow. +// Assumes a global environment does not yet exist. // -// Returns the new array of lines describing the workflow after inserting the new envvar. -func addEnvvarToGlobalEnv( - lines []string, - existingEnvvars map[string]string, - pattern unsafePattern, unsafeVar string, -) ([]string, error) { - globalIndentation, err := findGlobalIndentation(lines) +// Returns: +// - []string: the new array of lines describing the workflow, now with the global `env:` inserted. +// - int: the row where the `env:` block was added +func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, error) { + envPos, err := findNewEnvPos(lines, globalIndentation) if err != nil { - return lines, err - } - - if _, ok := existingEnvvars[unsafeVar]; ok { - // an existing envvar already handles this unsafe var, we can simply use it - return lines, nil + return nil, -1, err } - var insertPos, envvarIndent int - if len(existingEnvvars) > 0 { - insertPos, envvarIndent = findExistingEnv(lines, globalIndentation) - } else { - lines, insertPos, err = addNewGlobalEnv(lines, globalIndentation) - if err != nil { - return lines, err - } + label := strings.Repeat(" ", globalIndentation) + "env:" + content := []string{label} - // position now points to `env:`, insert variables below it - insertPos++ - envvarIndent = globalIndentation + getDefaultIndentStep(lines) + numBlankLines := getDefaultBlockSpacing(lines, globalIndentation) + for i := 0; i < numBlankLines; i++ { + content = append(content, "") } - envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) - lines = slices.Insert(lines, insertPos, strings.Repeat(" ", envvarIndent)+envvarDefinition) - - return lines, nil + lines = slices.Insert(lines, envPos, content...) + return lines, envPos, nil } -// Parses the envvars from the existing global `env:` block. -// Returns a map from the GitHub variable name to the envvar name (i.e. "github.event.issue.body": "ISSUE_BODY"). -func parseExistingEnvvars(workflow *actionlint.Workflow) map[string]string { - envvars := make(map[string]string) - - if workflow.Env == nil { - return envvars - } - - r := regexp.MustCompile(`\$\{\{\s*(github\.[^\s]*?)\s*}}`) - for _, v := range workflow.Env.Vars { - value := v.Value.Value - - if strings.Contains(value, "${{") { - // extract simple variable definition (without brackets, etc) - m := r.FindStringSubmatch(value) - if len(m) == 2 { - value = m[1] - envvars[value] = v.Name.Value - } else { - envvars[v.Value.Value] = v.Name.Value - } - } else { - envvars[v.Value.Value] = v.Name.Value +// Returns the line where a new `env:` block should be inserted: right above the `jobs:` label. +func findNewEnvPos(lines []string, globalIndent int) (int, error) { + jobsRegex := labelRegex("jobs", globalIndent) + for i, line := range lines { + if jobsRegex.MatchString(line) { + return i, nil } } - return envvars -} - -// Replaces all instances of the given script injection variable with the safe environment variable. -func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) { - runIndent := getIndent(lines[runIndex]) - for i, line := range lines[runIndex:] { - currLine := int(runIndex) + i - if i > 0 && isParentLevelIndent(lines[currLine], runIndent) { - // anything at the same indent as the first line of the `- run:` block will mean the end of the run block. - break - } - lines[currLine] = pattern.replaceRegex.ReplaceAllString(line, pattern.envvarName) - } + return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine location for new environment") } -func getUnsafePattern(unsafeVar string) (unsafePattern, error) { - unsafePatterns := []unsafePattern{ - newUnsafePattern("AUTHOR_EMAIL", `github\.event\.commits.*?\.author\.email`), - newUnsafePattern("AUTHOR_EMAIL", `github\.event\.head_commit\.author\.email`), - newUnsafePattern("AUTHOR_NAME", `github\.event\.commits.*?\.author\.name`), - newUnsafePattern("AUTHOR_NAME", `github\.event\.head_commit\.author\.name`), - newUnsafePattern("COMMENT_BODY", `github\.event\.comment\.body`), - newUnsafePattern("COMMIT_MESSAGE", `github\.event\.commits.*?\.message`), - newUnsafePattern("COMMIT_MESSAGE", `github\.event\.head_commit\.message`), - newUnsafePattern("DISCUSSION_TITLE", `github\.event\.discussion\.title`), - newUnsafePattern("DISCUSSION_BODY", `github\.event\.discussion\.body`), - newUnsafePattern("ISSUE_BODY", `github\.event\.issue\.body`), - newUnsafePattern("ISSUE_COMMENT_BODY", `github\.event\.issue_comment\.comment\.body`), - newUnsafePattern("ISSUE_TITLE", `github\.event\.issue\.title`), - newUnsafePattern("PAGE_NAME", `github\.event\.pages.*?\.page_name`), - newUnsafePattern("PR_BODY", `github\.event\.pull_request\.body`), - newUnsafePattern("PR_DEFAULT_BRANCH", `github\.event\.pull_request\.head\.repo\.default_branch`), - newUnsafePattern("PR_HEAD_LABEL", `github\.event\.pull_request\.head\.label`), - newUnsafePattern("PR_HEAD_REF", `github\.event\.pull_request\.head\.ref`), - newUnsafePattern("PR_TITLE", `github\.event\.pull_request\.title`), - newUnsafePattern("REVIEW_BODY", `github\.event\.review\.body`), - newUnsafePattern("REVIEW_COMMENT_BODY", `github\.event\.review_comment\.body`), - - newUnsafePattern("HEAD_REF", `github\.head_ref`), - } - - for _, p := range unsafePatterns { - p := p - if p.idRegex.MatchString(unsafeVar) { - arrayVarRegex := regexp.MustCompile(`\[(.+?)\]`) - arrayIdx := arrayVarRegex.FindStringSubmatch(unsafeVar) - if arrayIdx == nil || len(arrayIdx) < 2 { - // not an array variable, the default envvar name is sufficient. - return p, nil - } - // Array variable (i.e. `github.event.commits[0].message`), must avoid potential conflicts. - // Add the array index to the name as a suffix, and use the exact unsafe variable name instead of the - // default, which includes a regex that will catch all instances of the array. - return newUnsafePattern(p.envvarName+"_"+arrayIdx[1], regexp.QuoteMeta(unsafeVar)), nil +// Returns the "global" indentation, as defined by the indentation on the required `on:` block. +// Will equal 0 in almost all cases. +func findGlobalIndentation(lines []string) (int, error) { + r := regexp.MustCompile(`^\s*on:`) + for _, line := range lines { + if r.MatchString(line) { + return getIndent(line), nil } } - return unsafePattern{}, sce.WithMessage(sce.ErrScorecardInternal, - fmt.Sprintf("Unknown dangerous variable: %s", unsafeVar)) + return -1, sce.WithMessage(sce.ErrScorecardInternal, "Could not determine global indentation") } // Returns the indentation of the given line. The indentation is all leading whitespace and dashes. diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go deleted file mode 100644 index ad10ca811b0..00000000000 --- a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2024 OpenSSF 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 patch diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go deleted file mode 100644 index ad10ca811b0..00000000000 --- a/probes/hasDangerousWorkflowScriptInjection/patch/parse_workflow_test.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2024 OpenSSF 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 patch From 71d73a417edf347435852bef0d4475ff2d8afad3 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 29 Aug 2024 20:00:40 +0000 Subject: [PATCH 22/31] Define localPath in runScorecard Signed-off-by: Pedro Kaj Kjellerup Nacht --- cmd/internal/scdiff/app/runner/runner_test.go | 1 + pkg/scorecard/scorecard.go | 6 ++++++ pkg/scorecard/scorecard_result.go | 6 ------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/internal/scdiff/app/runner/runner_test.go b/cmd/internal/scdiff/app/runner/runner_test.go index 1a43c8e3f19..f8123f9af58 100644 --- a/cmd/internal/scdiff/app/runner/runner_test.go +++ b/cmd/internal/scdiff/app/runner/runner_test.go @@ -46,6 +46,7 @@ func TestRunner_Run(t *testing.T) { mockRepo.EXPECT().GetDefaultBranchName().Return("main", nil) mockRepo.EXPECT().Close().Return(nil) mockRepo.EXPECT().GetFileReader(gomock.Any()).Return(nil, errors.New("reading files unsupported for this test")).AnyTimes() + mockRepo.EXPECT().LocalPath().Return(".", nil) r := Runner{ ctx: context.Background(), // use a check which works locally, but we declare no files above so no-op diff --git a/pkg/scorecard/scorecard.go b/pkg/scorecard/scorecard.go index 8cb113fcf34..d3a73291d4e 100644 --- a/pkg/scorecard/scorecard.go +++ b/pkg/scorecard/scorecard.go @@ -138,6 +138,11 @@ func runScorecard(ctx context.Context, resultsCh := make(chan checker.CheckResult) + localPath, err := repoClient.LocalPath() + if err != nil { + return Result{}, fmt.Errorf("RepoClient.LocalPath: %w", err) + } + // Set metadata for all checks to use. This is necessary // to create remediations from the probe yaml files. ret.RawResults.Metadata.Metadata = map[string]string{ @@ -146,6 +151,7 @@ func runScorecard(ctx context.Context, "repository.uri": repo.URI(), "repository.sha1": commitSHA, "repository.defaultBranch": defaultBranch, + "localPath": localPath, } request := &checker.CheckRequest{ diff --git a/pkg/scorecard/scorecard_result.go b/pkg/scorecard/scorecard_result.go index 9d0eb2871e6..6f925092135 100644 --- a/pkg/scorecard/scorecard_result.go +++ b/pkg/scorecard/scorecard_result.go @@ -395,12 +395,6 @@ func assignRawData(probeCheckName string, request *checker.CheckRequest, ret *Re } func populateRawResults(request *checker.CheckRequest, probesToRun []string, ret *Result) error { - localPath, err := request.RepoClient.LocalPath() - if err != nil { - return fmt.Errorf("RepoClient.LocalPath: %w", err) - } - ret.RawResults.Metadata.Metadata["localPath"] = localPath - seen := map[string]bool{} for _, probeName := range probesToRun { p, err := proberegistration.Get(probeName) From 938a59cb0ce62693d58374ffc76a5f09200b532f Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 29 Aug 2024 20:03:10 +0000 Subject: [PATCH 23/31] Assert valid offset, use TrimSpace, drop unused struct member Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/patch/impl.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 02093a03ef2..580dc3b22bc 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -34,7 +34,6 @@ type unsafePattern struct { idRegex *regexp.Regexp replaceRegex *regexp.Regexp envvarName string - ghVarName string } // Fixes the script injection identified by the finding and returns a unified diff users can apply (with `git apply` or @@ -58,7 +57,11 @@ func GeneratePatch( // Returns a patched version of the workflow without the script injection finding. func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow) (string, error) { - unsafeVar := strings.Trim(f.Snippet, " ") + unsafeVar := strings.TrimSpace(f.Snippet) + + if f.Offset <= 0 { + return "", sce.WithMessage(sce.ErrScorecardInternal, "Invalid dangerous workflow offset") + } runCmdIndex := f.Offset - 1 lines := strings.Split(content, "\n") @@ -134,7 +137,6 @@ func getUnsafePattern(unsafeVar string) (unsafePattern, error) { func newUnsafePattern(e, p string) unsafePattern { return unsafePattern{ envvarName: e, - ghVarName: p, // Regex to simply identify the unsafe variable that triggered the finding. // Must use a regex and not a simple string to identify possible uses of array variables // (i.e. `github.event.commits[0].author.email`). From fa8e16b6b8a0f58094871501d68b7a868798a79e Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 29 Aug 2024 20:03:37 +0000 Subject: [PATCH 24/31] Just use []bytes instead of string Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/impl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 4fb639447b7..f6806e72051 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -92,7 +92,7 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { } content = string(c) - workflow, errs = actionlint.Parse([]byte(content)) + workflow, errs = actionlint.Parse(c) if len(errs) > 0 && workflow == nil { continue } From 42cf837256b2b077cd13121879dc273a16b8afbc Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 29 Aug 2024 23:32:58 +0000 Subject: [PATCH 25/31] Use []byte, not string Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 9 +- .../patch/impl.go | 89 ++++++++++--------- .../patch/impl_test.go | 6 +- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index f6806e72051..fcf83e1bf0a 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -59,8 +59,8 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { var findings []finding.Finding var curr string - var content string var workflow *actionlint.Workflow + var content []byte var errs []*actionlint.Error localPath := raw.Metadata.Metadata["localPath"] for _, e := range r.Workflows { @@ -85,15 +85,14 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { wp := path.Join(localPath, e.File.Path) if curr != wp { curr = wp - var c []byte - c, err = os.ReadFile(wp) + content, err = os.ReadFile(wp) if err != nil { continue } - content = string(c) - workflow, errs = actionlint.Parse(c) + workflow, errs = actionlint.Parse(content) if len(errs) > 0 && workflow == nil { + // the workflow contains unrecoverable parsing errors, skip. continue } } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 580dc3b22bc..741bf817c26 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -14,6 +14,7 @@ package patch import ( + "bytes" "fmt" "regexp" "slices" @@ -40,7 +41,7 @@ type unsafePattern struct { // `patch`) to fix the workflow themselves. Should an error occur, an empty patch is returned. func GeneratePatch( f checker.File, - content string, + content []byte, workflow *actionlint.Workflow, workflowErrs []*actionlint.Error, ) (string, error) { @@ -56,36 +57,35 @@ func GeneratePatch( } // Returns a patched version of the workflow without the script injection finding. -func patchWorkflow(f checker.File, content string, workflow *actionlint.Workflow) (string, error) { +func patchWorkflow(f checker.File, content []byte, workflow *actionlint.Workflow) ([]byte, error) { unsafeVar := strings.TrimSpace(f.Snippet) if f.Offset <= 0 { - return "", sce.WithMessage(sce.ErrScorecardInternal, "Invalid dangerous workflow offset") + return []byte(""), sce.WithMessage(sce.ErrScorecardInternal, "Invalid dangerous workflow offset") } runCmdIndex := f.Offset - 1 - - lines := strings.Split(content, "\n") + lines := bytes.Split(content, []byte("\n")) unsafePattern, err := getUnsafePattern(unsafeVar) if err != nil { - return "", err + return []byte(""), err } existingEnvvars := parseExistingEnvvars(workflow) unsafePattern, err = useExistingEnvvars(unsafePattern, existingEnvvars, unsafeVar) if err != nil { - return "", err + return []byte(""), err } replaceUnsafeVarWithEnvvar(lines, unsafePattern, runCmdIndex) lines, err = addEnvvarToGlobalEnv(lines, existingEnvvars, unsafePattern, unsafeVar) if err != nil { - return "", sce.WithMessage(sce.ErrScorecardInternal, + return []byte(""), sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Unknown dangerous variable: %s", unsafeVar)) } - return strings.Join(lines, "\n"), nil + return bytes.Join(lines, []byte("\n")), nil } func getUnsafePattern(unsafeVar string) (unsafePattern, error) { @@ -126,7 +126,8 @@ func getUnsafePattern(unsafeVar string) (unsafePattern, error) { // Array variable (i.e. `github.event.commits[0].message`), must avoid potential conflicts. // Add the array index to the name as a suffix, and use the exact unsafe variable name instead of the // default, which includes a regex that will catch all instances of the array. - return newUnsafePattern(p.envvarName+"_"+arrayIdx[1], regexp.QuoteMeta(unsafeVar)), nil + envvarName := fmt.Sprintf("%s_%s", p.envvarName, arrayIdx[1]) + return newUnsafePattern(envvarName, regexp.QuoteMeta(unsafeVar)), nil } } @@ -208,7 +209,7 @@ func useExistingEnvvars( } // Replaces all instances of the given script injection variable with the safe environment variable. -func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex uint) { +func replaceUnsafeVarWithEnvvar(lines [][]byte, pattern unsafePattern, runIndex uint) { runIndent := getIndent(lines[runIndex]) for i, line := range lines[runIndex:] { currLine := int(runIndex) + i @@ -216,7 +217,7 @@ func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex // anything at the same indent as the first line of the `- run:` block will mean the end of the run block. break } - lines[currLine] = pattern.replaceRegex.ReplaceAllString(line, pattern.envvarName) + lines[currLine] = pattern.replaceRegex.ReplaceAll(line, []byte(pattern.envvarName)) } } @@ -225,10 +226,10 @@ func replaceUnsafeVarWithEnvvar(lines []string, pattern unsafePattern, runIndex // // Returns the new array of lines describing the workflow after inserting the new envvar. func addEnvvarToGlobalEnv( - lines []string, + lines [][]byte, existingEnvvars map[string]string, pattern unsafePattern, unsafeVar string, -) ([]string, error) { +) ([][]byte, error) { globalIndentation, err := findGlobalIndentation(lines) if err != nil { return lines, err @@ -254,7 +255,7 @@ func addEnvvarToGlobalEnv( } envvarDefinition := fmt.Sprintf("%s: ${{ %s }}", pattern.envvarName, unsafeVar) - lines = slices.Insert(lines, insertPos, strings.Repeat(" ", envvarIndent)+envvarDefinition) + lines = slices.Insert(lines, insertPos, append(bytes.Repeat([]byte(" "), envvarIndent), []byte(envvarDefinition)...)) return lines, nil } @@ -266,12 +267,12 @@ func addEnvvarToGlobalEnv( // - int: the indentation used for the declared environment variables // // Both values return -1 if the `env` block doesn't exist or is invalid. -func findExistingEnv(lines []string, globalIndent int) (int, int) { +func findExistingEnv(lines [][]byte, globalIndent int) (int, int) { var currPos int - var line string + var line []byte envRegex := labelRegex("env", globalIndent) for currPos, line = range lines { - if envRegex.MatchString(line) { + if envRegex.Match(line) { break } } @@ -307,18 +308,18 @@ func findExistingEnv(lines []string, globalIndent int) (int, int) { // Returns: // - []string: the new array of lines describing the workflow, now with the global `env:` inserted. // - int: the row where the `env:` block was added -func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, error) { +func addNewGlobalEnv(lines [][]byte, globalIndentation int) ([][]byte, int, error) { envPos, err := findNewEnvPos(lines, globalIndentation) if err != nil { return nil, -1, err } - label := strings.Repeat(" ", globalIndentation) + "env:" - content := []string{label} + label := append(bytes.Repeat([]byte(" "), globalIndentation), []byte("env:")...) + content := [][]byte{label} numBlankLines := getDefaultBlockSpacing(lines, globalIndentation) for i := 0; i < numBlankLines; i++ { - content = append(content, "") + content = append(content, []byte("")) } lines = slices.Insert(lines, envPos, content...) @@ -326,10 +327,10 @@ func addNewGlobalEnv(lines []string, globalIndentation int) ([]string, int, erro } // Returns the line where a new `env:` block should be inserted: right above the `jobs:` label. -func findNewEnvPos(lines []string, globalIndent int) (int, error) { +func findNewEnvPos(lines [][]byte, globalIndent int) (int, error) { jobsRegex := labelRegex("jobs", globalIndent) for i, line := range lines { - if jobsRegex.MatchString(line) { + if jobsRegex.Match(line) { return i, nil } } @@ -339,10 +340,10 @@ func findNewEnvPos(lines []string, globalIndent int) (int, error) { // Returns the "global" indentation, as defined by the indentation on the required `on:` block. // Will equal 0 in almost all cases. -func findGlobalIndentation(lines []string) (int, error) { +func findGlobalIndentation(lines [][]byte) (int, error) { r := regexp.MustCompile(`^\s*on:`) for _, line := range lines { - if r.MatchString(line) { + if r.Match(line) { return getIndent(line), nil } } @@ -351,19 +352,19 @@ func findGlobalIndentation(lines []string) (int, error) { } // Returns the indentation of the given line. The indentation is all leading whitespace and dashes. -func getIndent(line string) int { - return len(line) - len(strings.TrimLeft(line, " -")) +func getIndent(line []byte) int { + return len(line) - len(bytes.TrimLeft(line, " -")) } // Returns the "default" number of blank lines between blocks. // The default is taken as the number of blank lines between the `jobs` label and the end of the preceding block. -func getDefaultBlockSpacing(lines []string, globalIndent int) int { +func getDefaultBlockSpacing(lines [][]byte, globalIndent int) int { jobsRegex := labelRegex("jobs", globalIndent) var jobsIdx int - var line string + var line []byte for jobsIdx, line = range lines { - if jobsRegex.MatchString(line) { + if jobsRegex.Match(line) { break } } @@ -384,18 +385,18 @@ func getDefaultBlockSpacing(lines []string, globalIndent int) int { } // Returns whether the given line is a blank line (empty or only whitespace). -func isBlank(line string) bool { +func isBlank(line []byte) bool { blank := regexp.MustCompile(`^\s*$`) - return blank.MatchString(line) + return blank.Match(line) } // Returns whether the given line only contains comments. -func isComment(line string) bool { +func isComment(line []byte) bool { comment := regexp.MustCompile(`^\s*#`) - return comment.MatchString(line) + return comment.Match(line) } -func isBlankOrComment(line string) bool { +func isBlankOrComment(line []byte) bool { return isBlank(line) || isComment(line) } @@ -410,7 +411,7 @@ func isBlankOrComment(line string) bool { // job_bar: # this line has job_foo's indentation, so we know job_foo is done // // Blank lines and those containing only comments are ignored and always return false. -func isParentLevelIndent(line string, parentIndent int) bool { +func isParentLevelIndent(line []byte, parentIndent int) bool { if isBlankOrComment(line) { return false } @@ -423,11 +424,11 @@ func labelRegex(label string, indent int) *regexp.Regexp { // Returns the default indentation step adopted in the document. // This is taken from the difference in indentation between the `jobs:` label and the first job's label. -func getDefaultIndentStep(lines []string) int { +func getDefaultIndentStep(lines [][]byte) int { jobs := regexp.MustCompile(`^\s*jobs:`) var jobsIndex, jobsIndent int for i, line := range lines { - if jobs.MatchString(line) { + if jobs.Match(line) { jobsIndex = i jobsIndent = getIndent(line) break @@ -450,8 +451,8 @@ func getDefaultIndentStep(lines []string) int { // errors, then the patched version also might. As long as all the patch's errors match the original's, it is validated. // // Returns the array of new parsing errors caused by the patch. -func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) []*actionlint.Error { - _, patchedErrs := actionlint.Parse([]byte(content)) +func validatePatchedWorkflow(content []byte, originalErrs []*actionlint.Error) []*actionlint.Error { + _, patchedErrs := actionlint.Parse(content) if len(patchedErrs) == 0 { return []*actionlint.Error{} } @@ -495,7 +496,7 @@ func validatePatchedWorkflow(content string, originalErrs []*actionlint.Error) [ } // Returns the changes between the original and patched workflows as a unified diff (same as `git diff` or `diff -u`). -func getDiff(path, original, patched string) (string, error) { +func getDiff(path string, original, patched []byte) (string, error) { // initialize an in-memory repository repo, err := newInMemoryRepo() if err != nil { @@ -528,7 +529,7 @@ func newInMemoryRepo() (*git.Repository, error) { } // Commits the workflow at the given path to the in-memory repository. -func commitWorkflow(path, contents string, repo *git.Repository) (*object.Commit, error) { +func commitWorkflow(path string, contents []byte, repo *git.Repository) (*object.Commit, error) { worktree, err := repo.Worktree() if err != nil { return nil, fmt.Errorf("repo.Worktree: %w", err) @@ -541,7 +542,7 @@ func commitWorkflow(path, contents string, repo *git.Repository) (*object.Commit return nil, fmt.Errorf("filesystem.Create: %w", err) } - _, err = df.Write([]byte(contents)) + _, err = df.Write(contents) if err != nil { return nil, fmt.Errorf("df.Write: %w", err) } diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go index 12f93976152..6ccff5e6a5d 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go @@ -144,7 +144,7 @@ func Test_patchWorkflow(t *testing.T) { for i, dw := range dws { i++ // Only used for error messages, increment for legibility - output, err := patchWorkflow(dw.File, string(inputContent), workflow) + output, err := patchWorkflow(dw.File, inputContent, workflow) if err != nil { t.Errorf("Couldn't patch workflow for finding #%d. Error:\n%s", i, err) } @@ -183,7 +183,7 @@ func readWorkflow(filePath string) ([]byte, *actionlint.Workflow, []*actionlint. return inputContent, workflow, inputErrs, nil } -func getExpected(t *testing.T, filePath string, numFindings, findingIndex int) string { +func getExpected(t *testing.T, filePath string, numFindings, findingIndex int) []byte { t.Helper() // build path to fixed version dot := strings.LastIndex(filePath, ".") @@ -197,7 +197,7 @@ func getExpected(t *testing.T, filePath string, numFindings, findingIndex int) s if err != nil { t.Errorf("Couldn't read expected output file for finding #%d. Error:\n%s", findingIndex, err) } - return string(content) + return content } func detectDangerousWorkflows(t *testing.T, filePath string) []checker.DangerousWorkflow { From 10e658903fd9e14134862ef6cef8120fbc2bb941 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 29 Aug 2024 23:33:31 +0000 Subject: [PATCH 26/31] go mod tidy updates Signed-off-by: Pedro Kaj Kjellerup Nacht --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index ef50d3d4c2b..23f3c37afc4 100644 --- a/go.mod +++ b/go.mod @@ -160,7 +160,6 @@ require ( github.com/google/wire v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/hexops/gotextdiff v1.0.3 github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect From fb31f93e7d0eb900a3dcf6a1b4092dfe076cea0d Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Wed, 4 Sep 2024 15:36:07 +0000 Subject: [PATCH 27/31] Ensure valid offset Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/patch/impl.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go index 741bf817c26..4c10fbb1de5 100644 --- a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/patch/impl.go @@ -60,11 +60,11 @@ func GeneratePatch( func patchWorkflow(f checker.File, content []byte, workflow *actionlint.Workflow) ([]byte, error) { unsafeVar := strings.TrimSpace(f.Snippet) - if f.Offset <= 0 { + lines := bytes.Split(content, []byte("\n")) + if f.Offset == 0 || int(f.Offset) >= len(lines) { return []byte(""), sce.WithMessage(sce.ErrScorecardInternal, "Invalid dangerous workflow offset") } runCmdIndex := f.Offset - 1 - lines := bytes.Split(content, []byte("\n")) unsafePattern, err := getUnsafePattern(unsafeVar) if err != nil { From d6e4fd114f24949f06d961beebf2b7d4c6e5ef5c Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Wed, 4 Sep 2024 15:40:59 +0000 Subject: [PATCH 28/31] Move /patch to /internal/patch Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/impl.go | 2 +- .../{ => internal}/patch/impl.go | 0 .../{ => internal}/patch/impl_test.go | 0 .../{ => internal}/patch/testdata/arrayVariables.yaml | 0 .../{ => internal}/patch/testdata/arrayVariables_fixed_1.yaml | 0 .../{ => internal}/patch/testdata/arrayVariables_fixed_2.yaml | 0 .../{ => internal}/patch/testdata/crazyButValidIndentation.yaml | 0 .../patch/testdata/crazyButValidIndentation_fixed.yaml | 0 .../{ => internal}/patch/testdata/envVarNameAlreadyInUse.yaml | 0 .../patch/testdata/envVarNameAlreadyInUse_fixed.yaml | 0 .../patch/testdata/fourSpacesIndentationExistentEnvVar.yaml | 0 .../testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml | 0 .../patch/testdata/ignorePatternInsideComments.yaml | 0 .../{ => internal}/patch/testdata/newlineOnEOF.yaml | 0 .../{ => internal}/patch/testdata/newlineOnEOF_fixed.yaml | 0 .../patch/testdata/noLineBreaksBetweenBlocks.yaml | 0 .../patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml | 0 .../{ => internal}/patch/testdata/realExample1.yaml | 0 .../{ => internal}/patch/testdata/realExample1_fixed.yaml | 0 .../{ => internal}/patch/testdata/realExample2.yaml | 0 .../{ => internal}/patch/testdata/realExample2_fixed.yaml | 0 .../{ => internal}/patch/testdata/realExample3.yaml | 0 .../{ => internal}/patch/testdata/realExample3_fixed.yaml | 0 .../{ => internal}/patch/testdata/reuseEnvVarSmallerScope.yaml | 0 .../patch/testdata/reuseEnvVarSmallerScope_fixed.yaml | 0 .../{ => internal}/patch/testdata/reuseEnvVarWithDiffName.yaml | 0 .../patch/testdata/reuseEnvVarWithDiffName_fixed.yaml | 0 .../patch/testdata/reuseWorkflowLevelEnvVars.yaml | 0 .../patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml | 0 .../patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml | 0 .../patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml | 0 .../patch/testdata/twoInjectionsDifferentJobs.yaml | 0 .../patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml | 0 .../patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml | 0 .../{ => internal}/patch/testdata/twoInjectionsSameJob.yaml | 0 .../patch/testdata/twoInjectionsSameJob_fixed_1.yaml | 0 .../patch/testdata/twoInjectionsSameJob_fixed_2.yaml | 0 .../{ => internal}/patch/testdata/twoInjectionsSameStep.yaml | 0 .../patch/testdata/twoInjectionsSameStep_fixed_1.yaml | 0 .../patch/testdata/twoInjectionsSameStep_fixed_3.yaml | 0 .../patch/testdata/userInputAssignedToVariable.yaml | 0 .../patch/testdata/userInputAssignedToVariable_fixed.yaml | 0 42 files changed, 1 insertion(+), 1 deletion(-) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/impl.go (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/impl_test.go (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/arrayVariables.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/arrayVariables_fixed_1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/arrayVariables_fixed_2.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/crazyButValidIndentation.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/crazyButValidIndentation_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/envVarNameAlreadyInUse.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/envVarNameAlreadyInUse_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/ignorePatternInsideComments.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/newlineOnEOF.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/newlineOnEOF_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/noLineBreaksBetweenBlocks.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample1_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample2.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample2_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample3.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/realExample3_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseEnvVarSmallerScope.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseEnvVarWithDiffName.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseWorkflowLevelEnvVars.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsDifferentJobs.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameJob.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameJob_fixed_1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameJob_fixed_2.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameStep.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameStep_fixed_1.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/twoInjectionsSameStep_fixed_3.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/userInputAssignedToVariable.yaml (100%) rename probes/hasDangerousWorkflowScriptInjection/{ => internal}/patch/testdata/userInputAssignedToVariable_fixed.yaml (100%) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index fcf83e1bf0a..a3a6f5caf47 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -27,7 +27,7 @@ import ( "github.com/ossf/scorecard/v5/finding" "github.com/ossf/scorecard/v5/internal/checknames" "github.com/ossf/scorecard/v5/internal/probes" - "github.com/ossf/scorecard/v5/probes/hasDangerousWorkflowScriptInjection/patch" + "github.com/ossf/scorecard/v5/probes/hasDangerousWorkflowScriptInjection/internal/patch" "github.com/ossf/scorecard/v5/probes/internal/utils/uerror" ) diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/impl.go rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go b/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl_test.go similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/impl_test.go rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/impl_test.go diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables_fixed_1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables_fixed_1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables_fixed_2.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/arrayVariables_fixed_2.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/arrayVariables_fixed_2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/crazyButValidIndentation.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/crazyButValidIndentation.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/crazyButValidIndentation_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/crazyButValidIndentation_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/crazyButValidIndentation_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/envVarNameAlreadyInUse.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/envVarNameAlreadyInUse.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/envVarNameAlreadyInUse_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/envVarNameAlreadyInUse_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/envVarNameAlreadyInUse_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/fourSpacesIndentationExistentEnvVar.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/fourSpacesIndentationExistentEnvVar_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/ignorePatternInsideComments.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/ignorePatternInsideComments.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/ignorePatternInsideComments.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/newlineOnEOF.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/newlineOnEOF.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/newlineOnEOF_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/newlineOnEOF_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/newlineOnEOF_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/noLineBreaksBetweenBlocks.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/noLineBreaksBetweenBlocks.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/noLineBreaksBetweenBlocks_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample1_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample1_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample1_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample2.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample2_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample2_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample2_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample3.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample3.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample3_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/realExample3_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/realExample3_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarSmallerScope.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarSmallerScope.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarSmallerScope_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarWithDiffName.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarWithDiffName.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseEnvVarWithDiffName_fixed.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/reuseWorkflowLevelEnvVars_fixed_3.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs_fixed_1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsDifferentJobs_fixed_2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob_fixed_1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob_fixed_1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob_fixed_2.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameJob_fixed_2.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameJob_fixed_2.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep_fixed_1.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_1.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep_fixed_1.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_3.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep_fixed_3.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/twoInjectionsSameStep_fixed_3.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/twoInjectionsSameStep_fixed_3.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/userInputAssignedToVariable.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/userInputAssignedToVariable.yaml diff --git a/probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml b/probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/userInputAssignedToVariable_fixed.yaml similarity index 100% rename from probes/hasDangerousWorkflowScriptInjection/patch/testdata/userInputAssignedToVariable_fixed.yaml rename to probes/hasDangerousWorkflowScriptInjection/internal/patch/testdata/userInputAssignedToVariable_fixed.yaml From 5a7b390a3ed10d91d240aa4b8bd1fcc91e340625 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Wed, 4 Sep 2024 20:18:08 +0000 Subject: [PATCH 29/31] Document patch behavior and add patch to remediation in def.yml Signed-off-by: Pedro Kaj Kjellerup Nacht --- probes/hasDangerousWorkflowScriptInjection/def.yml | 7 ++++++- probes/hasDangerousWorkflowScriptInjection/impl.go | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/def.yml b/probes/hasDangerousWorkflowScriptInjection/def.yml index 7c4f482ae0c..692cedb42e7 100644 --- a/probes/hasDangerousWorkflowScriptInjection/def.yml +++ b/probes/hasDangerousWorkflowScriptInjection/def.yml @@ -20,7 +20,7 @@ motivation: > implementation: > The probe analyzes the repository's workflows for known dangerous patterns. outcome: - - The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. + - The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. Each finding includes a suggested patch to fix the respective script injection. - If no dangerous patterns are found, the probe returns one finding with OutcomeFalse. remediation: onOutcome: True @@ -30,6 +30,11 @@ remediation: markdown: - Avoid the dangerous workflow patterns. - See [this document](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections) for information on avoiding and mitigating the risk of script injections. + - | + Here is a proposed patch to eliminate this risk: + ```yml + ${{ metadata.patch }} + ``` ecosystem: languages: - all diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index a3a6f5caf47..69efee10118 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -101,6 +101,9 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { continue } f.WithPatch(&findingPatch) + f.WithRemediationMetadata(map[string]string{ + "patch": findingPatch, + }) } if len(findings) == 0 { return falseOutcome() From 557a1b4517cb849d82ff8fef19bd361d63adf959 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Mon, 30 Sep 2024 22:13:02 +0000 Subject: [PATCH 30/31] Updates from review Signed-off-by: Pedro Kaj Kjellerup Nacht --- docs/probes.md | 2 +- probes/hasDangerousWorkflowScriptInjection/def.yml | 2 +- probes/hasDangerousWorkflowScriptInjection/impl.go | 7 ++++--- .../internal/patch/impl.go | 9 +++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/probes.md b/docs/probes.md index 7bb01e85646..5bdbaf61ad4 100644 --- a/docs/probes.md +++ b/docs/probes.md @@ -194,7 +194,7 @@ If the probe finds no binary files, it returns a single OutcomeFalse. **Implementation**: The probe analyzes the repository's workflows for known dangerous patterns. -**Outcomes**: The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. +**Outcomes**: The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. Each finding may include a suggested patch to fix the respective script injection. If no dangerous patterns are found, the probe returns one finding with OutcomeFalse. diff --git a/probes/hasDangerousWorkflowScriptInjection/def.yml b/probes/hasDangerousWorkflowScriptInjection/def.yml index 692cedb42e7..3b0b0351702 100644 --- a/probes/hasDangerousWorkflowScriptInjection/def.yml +++ b/probes/hasDangerousWorkflowScriptInjection/def.yml @@ -20,7 +20,7 @@ motivation: > implementation: > The probe analyzes the repository's workflows for known dangerous patterns. outcome: - - The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. Each finding includes a suggested patch to fix the respective script injection. + - The probe returns one finding with OutcomeTrue for each dangerous script injection pattern detected. Each finding may include a suggested patch to fix the respective script injection. - If no dangerous patterns are found, the probe returns one finding with OutcomeFalse. remediation: onOutcome: True diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 69efee10118..5c82de58b30 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -58,7 +58,7 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { } var findings []finding.Finding - var curr string + var currWorkflow string var workflow *actionlint.Workflow var content []byte var errs []*actionlint.Error @@ -83,8 +83,9 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { findings = append(findings, *f) wp := path.Join(localPath, e.File.Path) - if curr != wp { - curr = wp + if currWorkflow != wp { + // update current open file if injection in different file + currWorkflow = wp content, err = os.ReadFile(wp) if err != nil { continue diff --git a/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go b/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go index 4c10fbb1de5..5f464039616 100644 --- a/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/internal/patch/impl.go @@ -61,10 +61,11 @@ func patchWorkflow(f checker.File, content []byte, workflow *actionlint.Workflow unsafeVar := strings.TrimSpace(f.Snippet) lines := bytes.Split(content, []byte("\n")) - if f.Offset == 0 || int(f.Offset) >= len(lines) { + runCmdIndex := int(f.Offset - 1) + + if runCmdIndex < 0 || runCmdIndex >= len(lines) { return []byte(""), sce.WithMessage(sce.ErrScorecardInternal, "Invalid dangerous workflow offset") } - runCmdIndex := f.Offset - 1 unsafePattern, err := getUnsafePattern(unsafeVar) if err != nil { @@ -209,10 +210,10 @@ func useExistingEnvvars( } // Replaces all instances of the given script injection variable with the safe environment variable. -func replaceUnsafeVarWithEnvvar(lines [][]byte, pattern unsafePattern, runIndex uint) { +func replaceUnsafeVarWithEnvvar(lines [][]byte, pattern unsafePattern, runIndex int) { runIndent := getIndent(lines[runIndex]) for i, line := range lines[runIndex:] { - currLine := int(runIndex) + i + currLine := runIndex + i if i > 0 && isParentLevelIndent(lines[currLine], runIndent) { // anything at the same indent as the first line of the `- run:` block will mean the end of the run block. break From 892c442cc142d0649cd603c1022180e3d91761c4 Mon Sep 17 00:00:00 2001 From: Pedro Kaj Kjellerup Nacht Date: Thu, 3 Oct 2024 21:53:20 +0000 Subject: [PATCH 31/31] Add patch to finding before adding to list of findings Signed-off-by: Pedro Kaj Kjellerup Nacht --- .../impl.go | 91 ++++++++++++------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/probes/hasDangerousWorkflowScriptInjection/impl.go b/probes/hasDangerousWorkflowScriptInjection/impl.go index 5c82de58b30..f0396849582 100644 --- a/probes/hasDangerousWorkflowScriptInjection/impl.go +++ b/probes/hasDangerousWorkflowScriptInjection/impl.go @@ -63,49 +63,34 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { var content []byte var errs []*actionlint.Error localPath := raw.Metadata.Metadata["localPath"] - for _, e := range r.Workflows { - e := e - if e.Type != checker.DangerousWorkflowScriptInjection { + for _, w := range r.Workflows { + w := w + if w.Type != checker.DangerousWorkflowScriptInjection { continue } + f, err := finding.NewWith(fs, Probe, - fmt.Sprintf("script injection with untrusted input '%v'", e.File.Snippet), + fmt.Sprintf("script injection with untrusted input '%v'", w.File.Snippet), nil, finding.OutcomeTrue) if err != nil { return nil, Probe, fmt.Errorf("create finding: %w", err) } + f = f.WithLocation(&finding.Location{ - Path: e.File.Path, - Type: e.File.Type, - LineStart: &e.File.Offset, - Snippet: &e.File.Snippet, + Path: w.File.Path, + Type: w.File.Type, + LineStart: &w.File.Offset, + Snippet: &w.File.Snippet, }) - findings = append(findings, *f) - wp := path.Join(localPath, e.File.Path) - if currWorkflow != wp { - // update current open file if injection in different file - currWorkflow = wp - content, err = os.ReadFile(wp) - if err != nil { - continue - } - - workflow, errs = actionlint.Parse(content) - if len(errs) > 0 && workflow == nil { - // the workflow contains unrecoverable parsing errors, skip. - continue - } - } - findingPatch, err := patch.GeneratePatch(e.File, content, workflow, errs) - if err != nil { - continue + err = parseWorkflow(localPath, &w, &currWorkflow, &content, &workflow, &errs) + if err == nil { + generatePatch(&w, content, workflow, errs, f) } - f.WithPatch(&findingPatch) - f.WithRemediationMetadata(map[string]string{ - "patch": findingPatch, - }) + + findings = append(findings, *f) } + if len(findings) == 0 { return falseOutcome() } @@ -113,6 +98,50 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { return findings, Probe, nil } +func parseWorkflow( + localPath string, + e *checker.DangerousWorkflow, + currWorkflow *string, + content *[]byte, + workflow **actionlint.Workflow, + errs *[]*actionlint.Error, +) error { + var err error + wp := path.Join(localPath, e.File.Path) + if *currWorkflow != wp { + // update current open file if injection in different file + *currWorkflow = wp + *content, err = os.ReadFile(wp) + if err != nil { + return err //nolint:wrapcheck // we only care about the error's existence + } + + *workflow, *errs = actionlint.Parse(*content) + if len(*errs) > 0 && *workflow == nil { + // the workflow contains unrecoverable parsing errors, skip. + return err //nolint:wrapcheck // we only care about the error's existence + } + } + return nil +} + +func generatePatch( + e *checker.DangerousWorkflow, + content []byte, + workflow *actionlint.Workflow, + errs []*actionlint.Error, + f *finding.Finding, +) { + findingPatch, err := patch.GeneratePatch(e.File, content, workflow, errs) + if err != nil { + return + } + f.WithPatch(&findingPatch) + f.WithRemediationMetadata(map[string]string{ + "patch": findingPatch, + }) +} + func falseOutcome() ([]finding.Finding, string, error) { f, err := finding.NewWith(fs, Probe, "Project does not have dangerous workflow(s) with possibility of script injection.", nil,