Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add machine-readable patch to fix script injections in workflows #4218

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b4ec86d
Merge pull request #1 from joycebrum/feature/setup-environment-for-dw…
diogoteles08 Jun 29, 2024
5ee165c
Merge pull request #3 from joycebrum/feat/connect-patch-generator-wit…
diogoteles08 Jun 29, 2024
bcb159e
Merge pull request #2 from joycebrum/test/initial-tests-for-dw-fix
diogoteles08 Jun 29, 2024
5bddd1a
Merge pull request #4 from joycebrum/feat/get-input-needed-to-generat…
joycebrum Jun 29, 2024
488d89a
impl.go: slight refactor to loop
pnacht Mar 8, 2024
93c2fba
Add envvars to existing or new env, still not replaced in `run`
pnacht Mar 6, 2024
0394b86
Replace unsafe variables in run commands, generate git diff
pnacht Mar 7, 2024
3c7f9c6
Rewrite test file
pnacht Mar 8, 2024
b299a47
Rewrite patch/impl.go
pnacht Apr 1, 2024
9a5e043
Update test workflows
pnacht Jun 28, 2024
3f8e2af
Use existing envvars, validate patched workflow
pnacht Jun 28, 2024
8b47fdd
Test for same injection in same step, leading to duplicate findings
pnacht Jun 29, 2024
c632590
Use existing envvars with different name but same meaning
pnacht Jun 29, 2024
5c986e8
Avoid conflicts with irrelevant but existing envvars
pnacht Jun 29, 2024
6534155
Use first job's indent to define envvar indent
pnacht Jun 29, 2024
bf26120
Refactor patch/impl_test
pnacht Jul 3, 2024
31ea054
patch/impl: Simplify unsafePatterns, use errors, docs, lint
pnacht Jul 4, 2024
e61d79a
Fix panic in hasScriptInjection test due to missing file
pnacht Jul 4, 2024
bbe6c85
Avoid duplicate envvars dealing with array variables
pnacht Jul 4, 2024
09d4b47
Adopt existing inter-block spacing for new env
pnacht Jul 4, 2024
89b73a3
chore: Tidy up function order, remove unused files
pnacht Jul 4, 2024
71d73a4
Define localPath in runScorecard
pnacht Aug 29, 2024
938a59c
Assert valid offset, use TrimSpace, drop unused struct member
pnacht Aug 29, 2024
fa8e16b
Just use []bytes instead of string
pnacht Aug 29, 2024
42cf837
Use []byte, not string
pnacht Aug 29, 2024
10e6589
go mod tidy updates
pnacht Aug 29, 2024
fb31f93
Ensure valid offset
pnacht Sep 4, 2024
d6e4fd1
Move /patch to /internal/patch
pnacht Sep 4, 2024
5a7b390
Document patch behavior and add patch to remediation in def.yml
pnacht Sep 4, 2024
557a1b4
Updates from review
pnacht Sep 30, 2024
892c442
Add patch to finding before adding to list of findings
pnacht Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/internal/scdiff/app/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/probes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/scorecard/scorecard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -146,6 +151,7 @@ func runScorecard(ctx context.Context,
"repository.uri": repo.URI(),
"repository.sha1": commitSHA,
"repository.defaultBranch": defaultBranch,
"localPath": localPath,
}

request := &checker.CheckRequest{
Expand Down
4 changes: 4 additions & 0 deletions pkg/scorecard/scorecard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
},
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion probes/hasDangerousWorkflowScriptInjection/def.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand All @@ -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 }}
```
pnacht marked this conversation as resolved.
Show resolved Hide resolved
ecosystem:
languages:
- all
Expand Down
95 changes: 79 additions & 16 deletions probes/hasDangerousWorkflowScriptInjection/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ package hasDangerousWorkflowScriptInjection
import (
"embed"
"fmt"
"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/internal/patch"
"github.com/ossf/scorecard/v5/probes/internal/utils/uerror"
)

Expand Down Expand Up @@ -53,23 +58,37 @@ func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
}

var findings []finding.Finding
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,
})
findings = append(findings, *f)
var currWorkflow string
var workflow *actionlint.Workflow
var content []byte
var errs []*actionlint.Error
localPath := raw.Metadata.Metadata["localPath"]
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'", w.File.Snippet),
nil, finding.OutcomeTrue)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}

f = f.WithLocation(&finding.Location{
Path: w.File.Path,
Type: w.File.Type,
LineStart: &w.File.Offset,
Snippet: &w.File.Snippet,
})

err = parseWorkflow(localPath, &w, &currWorkflow, &content, &workflow, &errs)
if err == nil {
generatePatch(&w, content, workflow, errs, f)
}

findings = append(findings, *f)
}

if len(findings) == 0 {
Expand All @@ -79,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,
Expand Down
3 changes: 3 additions & 0 deletions probes/hasDangerousWorkflowScriptInjection/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func Test_Run(t *testing.T) {
Workflows: []checker.DangerousWorkflow{
{
Type: checker.DangerousWorkflowScriptInjection,
File: checker.File{
Path: "patch/testdata/userInputAssignedToVariable.yaml",
},
},
},
},
Expand Down
Loading
Loading