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