mirror of
https://github.com/ossf/scorecard.git
synced 2024-08-15 11:20:30 +03:00
Using library to parse github workflows
This commit is contained in:
parent
f319aca82d
commit
3dc507b9e1
@ -150,3 +150,6 @@ linters-settings:
|
||||
- ptrToRefParam
|
||||
- typeUnparen
|
||||
- unnecessaryBlock
|
||||
wrapcheck:
|
||||
ignorePackageGlobs:
|
||||
- github.com/ossf/scorecard/v3/checks/fileparser
|
||||
|
21
checks/fileparser/errors.go
Normal file
21
checks/fileparser/errors.go
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.
|
||||
|
||||
package fileparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow")
|
@ -20,7 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/rhysd/actionlint"
|
||||
|
||||
sce "github.com/ossf/scorecard/v3/errors"
|
||||
)
|
||||
@ -31,110 +31,51 @@ const defaultShellNonWindows = "bash"
|
||||
// defaultShellWindows is the default shell used for GitHub workflow actions for Windows.
|
||||
const defaultShellWindows = "pwsh"
|
||||
|
||||
// Structure for workflow config.
|
||||
// We only declare the fields we need.
|
||||
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
type GitHubActionWorkflowConfig struct {
|
||||
Jobs map[string]GitHubActionWorkflowJob
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
// A Github Action Workflow Job.
|
||||
// We only declare the fields we need.
|
||||
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
// nolint: govet
|
||||
type GitHubActionWorkflowJob struct {
|
||||
Name string `yaml:"name"`
|
||||
Steps []GitHubActionWorkflowStep `yaml:"steps"`
|
||||
Defaults struct {
|
||||
Run struct {
|
||||
Shell string `yaml:"shell"`
|
||||
} `yaml:"run"`
|
||||
} `yaml:"defaults"`
|
||||
RunsOn stringOrSlice `yaml:"runs-on"`
|
||||
Strategy struct {
|
||||
// In most cases, the 'matrix' field will have a key of 'os' which is an array of strings, but there are
|
||||
// some repos that have something like: 'matrix: ${{ fromJson(needs.matrix.outputs.latest) }}'.
|
||||
Matrix interface{} `yaml:"matrix"`
|
||||
} `yaml:"strategy"`
|
||||
}
|
||||
|
||||
// A Github Action Workflow Step.
|
||||
// We only declare the fields we need.
|
||||
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
type GitHubActionWorkflowStep struct {
|
||||
Name string `yaml:"name"`
|
||||
ID string `yaml:"id"`
|
||||
Shell string `yaml:"shell"`
|
||||
Run string `yaml:"run"`
|
||||
If string `yaml:"if"`
|
||||
Uses stringWithLine `yaml:"uses"`
|
||||
}
|
||||
|
||||
// stringOrSlice is for fields that can be a single string or a slice of strings. If the field is a single string,
|
||||
// this value will be a slice with a single string item.
|
||||
type stringOrSlice []string
|
||||
|
||||
func (s *stringOrSlice) UnmarshalYAML(value *yaml.Node) error {
|
||||
var stringSlice []string
|
||||
err := value.Decode(&stringSlice)
|
||||
if err == nil {
|
||||
*s = stringSlice
|
||||
// FormatActionlintError combines the errors into a single one.
|
||||
func FormatActionlintError(errs []*actionlint.Error) error {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var single string
|
||||
err = value.Decode(&single)
|
||||
if err != nil {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringOrSlice Value: %v", err))
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString(errInvalidGitHubWorkflow.Error() + ":")
|
||||
for _, err := range errs {
|
||||
builder.WriteString("\n" + err.Error())
|
||||
}
|
||||
*s = []string{single}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stringWithLine is for when you want to keep track of the line number that the string came from.
|
||||
type stringWithLine struct {
|
||||
Value string
|
||||
Line int
|
||||
}
|
||||
|
||||
func (ws *stringWithLine) UnmarshalYAML(value *yaml.Node) error {
|
||||
err := value.Decode(&ws.Value)
|
||||
if err != nil {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringWithLine Value: %v", err))
|
||||
}
|
||||
ws.Line = value.Line
|
||||
|
||||
return nil
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, builder.String())
|
||||
}
|
||||
|
||||
// GetOSesForJob returns the OSes this job runs on.
|
||||
func GetOSesForJob(job *GitHubActionWorkflowJob) ([]string, error) {
|
||||
func GetOSesForJob(job *actionlint.Job) ([]string, error) {
|
||||
// The 'runs-on' field either lists the OS'es directly, or it can have an expression '${{ matrix.os }}' which
|
||||
// is where the OS'es are actually listed.
|
||||
getFromMatrix := len(job.RunsOn) == 1 && strings.Contains(job.RunsOn[0], "matrix.os")
|
||||
if !getFromMatrix {
|
||||
return job.RunsOn, nil
|
||||
}
|
||||
jobOSes := make([]string, 0)
|
||||
// nolint: nestif
|
||||
if m, ok := job.Strategy.Matrix.(map[string]interface{}); ok {
|
||||
if osVal, ok := m["os"]; ok {
|
||||
if oses, ok := osVal.([]interface{}); ok {
|
||||
for _, os := range oses {
|
||||
if strVal, ok := os.(string); ok {
|
||||
jobOSes = append(jobOSes, strVal)
|
||||
}
|
||||
}
|
||||
return jobOSes, nil
|
||||
}
|
||||
getFromMatrix := len(job.RunsOn.Labels) == 1 && strings.Contains(job.RunsOn.Labels[0].Value, "matrix.os")
|
||||
if !getFromMatrix {
|
||||
// We can get the OSes straight from 'runs-on'.
|
||||
for _, os := range job.RunsOn.Labels {
|
||||
jobOSes = append(jobOSes, os.Value)
|
||||
}
|
||||
return jobOSes, nil
|
||||
}
|
||||
|
||||
for rowKey, rowValue := range job.Strategy.Matrix.Rows {
|
||||
if rowKey != "os" {
|
||||
continue
|
||||
}
|
||||
for _, os := range rowValue.Values {
|
||||
jobOSes = append(jobOSes, strings.Trim(os.String(), "'\""))
|
||||
}
|
||||
}
|
||||
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to determine OS for job: %v", job.Name))
|
||||
|
||||
if len(jobOSes) == 0 {
|
||||
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to determine OS for job: %v", job.Name.Value))
|
||||
}
|
||||
return jobOSes, nil
|
||||
}
|
||||
|
||||
// JobAlwaysRunsOnWindows returns true if the only OS that this job runs on is Windows.
|
||||
func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) {
|
||||
func JobAlwaysRunsOnWindows(job *actionlint.Job) (bool, error) {
|
||||
jobOSes, err := GetOSesForJob(job)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -148,13 +89,27 @@ func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) {
|
||||
}
|
||||
|
||||
// GetShellForStep returns the shell that is used to run the given step.
|
||||
func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJob) (string, error) {
|
||||
func GetShellForStep(step *actionlint.Step, job *actionlint.Job) (string, error) {
|
||||
// https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell.
|
||||
if step.Shell != "" {
|
||||
return step.Shell, nil
|
||||
execRun, ok := step.Exec.(*actionlint.ExecRun)
|
||||
if !ok {
|
||||
jobName := ""
|
||||
if job.Name != nil {
|
||||
jobName = job.Name.Value
|
||||
}
|
||||
stepName := ""
|
||||
if step.Name != nil {
|
||||
stepName = step.Name.Value
|
||||
}
|
||||
return "", sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName))
|
||||
}
|
||||
if job.Defaults.Run.Shell != "" {
|
||||
return job.Defaults.Run.Shell, nil
|
||||
if execRun != nil && execRun.Shell != nil && execRun.Shell.Value != "" {
|
||||
return execRun.Shell.Value, nil
|
||||
}
|
||||
if job.Defaults != nil && job.Defaults.Run != nil && job.Defaults.Run.Shell != nil &&
|
||||
job.Defaults.Run.Shell.Value != "" {
|
||||
return job.Defaults.Run.Shell.Value, nil
|
||||
}
|
||||
|
||||
isStepWindows, err := IsStepWindows(step)
|
||||
@ -177,7 +132,10 @@ func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJo
|
||||
}
|
||||
|
||||
// IsStepWindows returns true if the step will be run on Windows.
|
||||
func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) {
|
||||
func IsStepWindows(step *actionlint.Step) (bool, error) {
|
||||
if step.If == nil {
|
||||
return false, nil
|
||||
}
|
||||
windowsRegexes := []string{
|
||||
// Looking for "if: runner.os == 'Windows'" (and variants)
|
||||
`(?i)runner\.os\s*==\s*['"]windows['"]`,
|
||||
@ -188,7 +146,7 @@ func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) {
|
||||
}
|
||||
|
||||
for _, windowsRegex := range windowsRegexes {
|
||||
matches, err := regexp.MatchString(windowsRegex, step.If)
|
||||
matches, err := regexp.MatchString(windowsRegex, step.If.Value)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err))
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/rhysd/actionlint"
|
||||
"gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
@ -113,17 +113,17 @@ func TestGitHubWorkflowShell(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("cannot read file: %v", err)
|
||||
}
|
||||
var workflow GitHubActionWorkflowConfig
|
||||
err = yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
t.Errorf("cannot unmarshal file: %v", err)
|
||||
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
t.Errorf("cannot unmarshal file: %v", errs[0])
|
||||
}
|
||||
actualShells := make([]string, 0)
|
||||
for _, job := range workflow.Jobs {
|
||||
job := job
|
||||
for _, step := range job.Steps {
|
||||
step := step
|
||||
shell, err := GetShellForStep(&step, &job)
|
||||
shell, err := GetShellForStep(step, job)
|
||||
if err != nil {
|
||||
t.Errorf("error getting shell: %v", err)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/rhysd/actionlint"
|
||||
|
||||
"github.com/ossf/scorecard/v3/checker"
|
||||
"github.com/ossf/scorecard/v3/checks/fileparser"
|
||||
@ -53,25 +53,24 @@ func TokenPermissions(c *checker.CheckRequest) checker.CheckResult {
|
||||
return createResultForLeastPrivilegeTokens(data, err)
|
||||
}
|
||||
|
||||
func validatePermission(key string, value interface{}, path string,
|
||||
func validatePermission(permissionKey string, permissionValue *actionlint.PermissionScope, path string,
|
||||
dl checker.DetailLogger, pPermissions map[string]bool,
|
||||
ignoredPermissions map[string]bool) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
if permissionValue.Value == nil {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
|
||||
}
|
||||
|
||||
val := permissionValue.Value.Value
|
||||
if strings.EqualFold(val, "write") {
|
||||
if isPermissionOfInterest(key, ignoredPermissions) {
|
||||
if isPermissionOfInterest(permissionKey, ignoredPermissions) {
|
||||
dl.Warn3(&checker.LogMessage{
|
||||
Path: path,
|
||||
Type: checker.FileTypeSource,
|
||||
// TODO: set line.
|
||||
Offset: 1,
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
|
||||
// TODO: set Snippet.
|
||||
})
|
||||
recordPermissionWrite(key, pPermissions)
|
||||
recordPermissionWrite(permissionKey, pPermissions)
|
||||
} else {
|
||||
// Only log for debugging, otherwise
|
||||
// it may confuse users.
|
||||
@ -80,7 +79,7 @@ func validatePermission(key string, value interface{}, path string,
|
||||
Type: checker.FileTypeSource,
|
||||
// TODO: set line.
|
||||
Offset: 1,
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
|
||||
// TODO: set Snippet.
|
||||
})
|
||||
}
|
||||
@ -92,22 +91,16 @@ func validatePermission(key string, value interface{}, path string,
|
||||
Type: checker.FileTypeSource,
|
||||
// TODO: set line correctly.
|
||||
Offset: 1,
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
|
||||
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
|
||||
// TODO: set Snippet.
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMapPermissions(values map[interface{}]interface{}, path string,
|
||||
func validateMapPermissions(scopes map[string]*actionlint.PermissionScope, path string,
|
||||
dl checker.DetailLogger, pPermissions map[string]bool,
|
||||
ignoredPermissions map[string]bool) error {
|
||||
// Iterate over the permission, verify keys and values are strings.
|
||||
for k, v := range values {
|
||||
key, ok := k.(string)
|
||||
if !ok {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
|
||||
}
|
||||
|
||||
for key, v := range scopes {
|
||||
if err := validatePermission(key, v, path, dl, pPermissions, ignoredPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -125,14 +118,12 @@ func recordAllPermissionsWrite(pPermissions map[string]bool) {
|
||||
pPermissions["all"] = true
|
||||
}
|
||||
|
||||
func validatePermissions(permissions interface{}, path string,
|
||||
func validatePermissions(permissions *actionlint.Permissions, path string,
|
||||
dl checker.DetailLogger, pPermissions map[string]bool,
|
||||
ignoredPermissions map[string]bool) error {
|
||||
// Check the type of our values.
|
||||
switch val := permissions.(type) {
|
||||
// Empty string is nil type.
|
||||
// It defaults to 'none'
|
||||
case nil:
|
||||
allIsSet := permissions != nil && permissions.All != nil && permissions.All.Value != ""
|
||||
scopeIsSet := permissions != nil && len(permissions.Scopes) > 0
|
||||
if permissions == nil || (!allIsSet && !scopeIsSet) {
|
||||
dl.Info3(&checker.LogMessage{
|
||||
Path: path,
|
||||
Type: checker.FileTypeSource,
|
||||
@ -141,8 +132,9 @@ func validatePermissions(permissions interface{}, path string,
|
||||
Text: "permissions set to 'none'",
|
||||
// TODO: set Snippet.
|
||||
})
|
||||
// String type.
|
||||
case string:
|
||||
}
|
||||
if allIsSet {
|
||||
val := permissions.All.Value
|
||||
if !strings.EqualFold(val, "read-all") && val != "" {
|
||||
dl.Warn3(&checker.LogMessage{
|
||||
Path: path,
|
||||
@ -164,25 +156,17 @@ func validatePermissions(permissions interface{}, path string,
|
||||
Text: fmt.Sprintf("permissions set to '%v'", val),
|
||||
// TODO: set Snippet.
|
||||
})
|
||||
|
||||
// Map type.
|
||||
case map[interface{}]interface{}:
|
||||
if err := validateMapPermissions(val, path, dl, pPermissions, ignoredPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalid type.
|
||||
default:
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
|
||||
} else /* scopeIsSet == true */ if err := validateMapPermissions(permissions.Scopes, path, dl, pPermissions,
|
||||
ignoredPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTopLevelPermissions(config map[interface{}]interface{}, path string,
|
||||
func validateTopLevelPermissions(workflow *actionlint.Workflow, path string,
|
||||
dl checker.DetailLogger, pdata *permissionCbData) error {
|
||||
// Check if permissions are set explicitly.
|
||||
permissions, ok := config["permissions"]
|
||||
if !ok {
|
||||
if workflow.Permissions == nil {
|
||||
dl.Warn3(&checker.LogMessage{
|
||||
Path: path,
|
||||
Type: checker.FileTypeSource,
|
||||
@ -193,35 +177,18 @@ func validateTopLevelPermissions(config map[interface{}]interface{}, path string
|
||||
return nil
|
||||
}
|
||||
|
||||
return validatePermissions(permissions, path, dl,
|
||||
return validatePermissions(workflow.Permissions, path, dl,
|
||||
pdata.topLevelWritePermissions, map[string]bool{})
|
||||
}
|
||||
|
||||
func validateRunLevelPermissions(config map[interface{}]interface{}, path string,
|
||||
func validateRunLevelPermissions(workflow *actionlint.Workflow, path string,
|
||||
dl checker.DetailLogger, pdata *permissionCbData,
|
||||
ignoredPermissions map[string]bool) error {
|
||||
var jobs interface{}
|
||||
|
||||
jobs, ok := config["jobs"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
mjobs, ok := jobs.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
|
||||
}
|
||||
|
||||
for _, value := range mjobs {
|
||||
job, ok := value.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
|
||||
}
|
||||
for _, job := range workflow.Jobs {
|
||||
// Run-level permissions may be left undefined.
|
||||
// For most workflows, no write permissions are needed,
|
||||
// so only top-level read-only permissions need to be declared.
|
||||
permissions, ok := job["permissions"]
|
||||
if !ok {
|
||||
if job.Permissions == nil {
|
||||
dl.Debug3(&checker.LogMessage{
|
||||
Path: path,
|
||||
Type: checker.FileTypeSource,
|
||||
@ -230,8 +197,7 @@ func validateRunLevelPermissions(config map[interface{}]interface{}, path string
|
||||
})
|
||||
continue
|
||||
}
|
||||
err := validatePermissions(permissions, path, dl,
|
||||
pdata.runLevelWritePermissions, ignoredPermissions)
|
||||
err := validatePermissions(job.Permissions, path, dl, pdata.runLevelWritePermissions, ignoredPermissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -375,11 +341,9 @@ func validateGitHubActionTokenPermissions(path string, content []byte,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var workflow map[interface{}]interface{}
|
||||
err := yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
return false,
|
||||
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("yaml.Unmarshal: %v", err))
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
return false, fileparser.FormatActionlintError(errs)
|
||||
}
|
||||
|
||||
// 1. Top-level permission definitions.
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/rhysd/actionlint"
|
||||
|
||||
"github.com/ossf/scorecard/v3/checker"
|
||||
"github.com/ossf/scorecard/v3/checks/fileparser"
|
||||
@ -451,11 +451,11 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var workflow fileparser.GitHubActionWorkflowConfig
|
||||
err := yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("%v: %v", errInternalInvalidYamlFile, err))
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
// actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards.
|
||||
// Often we don't care about these errors.
|
||||
return false, fileparser.FormatActionlintError(errs)
|
||||
}
|
||||
|
||||
githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`)
|
||||
@ -465,12 +465,12 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
job := job
|
||||
for _, step := range job.Steps {
|
||||
step := step
|
||||
if step.Run == "" {
|
||||
if step.Exec.Kind() != actionlint.ExecKindRun {
|
||||
continue
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
|
||||
shell, err := fileparser.GetShellForStep(&step, &job)
|
||||
shell, err := fileparser.GetShellForStep(step, job)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -479,7 +479,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
continue
|
||||
}
|
||||
|
||||
run := step.Run
|
||||
run := step.Exec.(*actionlint.ExecRun).Run.Value
|
||||
// We replace the `${{ github.variable }}` to avoid shell parsing failures.
|
||||
script := githubVarRegex.ReplaceAll([]byte(run), []byte("GITHUB_REDACTED_VAR"))
|
||||
scriptContent = fmt.Sprintf("%v\n%v", scriptContent, string(script))
|
||||
@ -487,6 +487,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
}
|
||||
|
||||
if scriptContent != "" {
|
||||
var err error
|
||||
validated, err = validateShellFile(pathfn, []byte(scriptContent), dl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -534,30 +535,42 @@ func validateGitHubActionWorkflow(pathfn string, content []byte,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var workflow fileparser.GitHubActionWorkflowConfig
|
||||
err := yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("%v: %v", errInternalInvalidYamlFile, err))
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
// actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards.
|
||||
// Often we don't care about these errors.
|
||||
return false, fileparser.FormatActionlintError(errs)
|
||||
}
|
||||
|
||||
hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`)
|
||||
for jobName, job := range workflow.Jobs {
|
||||
if len(job.Name) > 0 {
|
||||
jobName = job.Name
|
||||
if job.Name != nil && len(job.Name.Value) > 0 {
|
||||
jobName = job.Name.Value
|
||||
}
|
||||
for _, step := range job.Steps {
|
||||
if len(step.Uses.Value) > 0 {
|
||||
// Ensure a hash at least as large as SHA1 is used (40 hex characters).
|
||||
// Example: action-name@hash
|
||||
match := hashRegex.Match([]byte(step.Uses.Value))
|
||||
if !match {
|
||||
dl.Warn3(&checker.LogMessage{
|
||||
Path: pathfn, Type: checker.FileTypeSource, Offset: step.Uses.Line, Snippet: step.Uses.Value,
|
||||
Text: fmt.Sprintf("dependency not pinned by hash (job '%v')", jobName),
|
||||
})
|
||||
if step.Exec.Kind() != actionlint.ExecKindAction {
|
||||
continue
|
||||
}
|
||||
execAction, ok := step.Exec.(*actionlint.ExecAction)
|
||||
if !ok {
|
||||
stepName := ""
|
||||
if step.Name != nil {
|
||||
stepName = step.Name.Value
|
||||
}
|
||||
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName))
|
||||
}
|
||||
// Ensure a hash at least as large as SHA1 is used (40 hex characters).
|
||||
// Example: action-name@hash
|
||||
match := hashRegex.Match([]byte(execAction.Uses.Value))
|
||||
if !match {
|
||||
dl.Warn3(&checker.LogMessage{
|
||||
Path: pathfn, Type: checker.FileTypeSource, Offset: execAction.Uses.Pos.Line, Snippet: execAction.Uses.Value,
|
||||
Text: fmt.Sprintf("dependency not pinned by hash (job '%v')", jobName),
|
||||
})
|
||||
}
|
||||
|
||||
githubOwned := fileparser.IsGitHubOwnedAction(execAction.Uses.Value)
|
||||
addWorkflowPinnedResult(pdata, match, githubOwned)
|
||||
}
|
||||
|
@ -933,7 +933,7 @@ func TestGitHubWorkflowUsesLineNumber(t *testing.T) {
|
||||
},
|
||||
{
|
||||
dependency: "docker/build-push-action@1.2.3",
|
||||
lineNumber: 26,
|
||||
lineNumber: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -20,7 +20,5 @@ jobs:
|
||||
steps:
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
run: echo "unpinned dependency"
|
||||
- name: some name
|
||||
run: echo "unpinned dependency"
|
||||
uses: docker/build-push-action@1.2.3
|
||||
|
12
go.mod
12
go.mod
@ -35,7 +35,10 @@ require (
|
||||
mvdan.cc/sh/v3 v3.4.0
|
||||
)
|
||||
|
||||
require gotest.tools v2.2.0+incompatible
|
||||
require (
|
||||
github.com/rhysd/actionlint v1.6.7
|
||||
gotest.tools v2.2.0+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.94.1 // indirect
|
||||
@ -52,6 +55,7 @@ require (
|
||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.3 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.12.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
@ -69,12 +73,16 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/klauspost/compress v1.13.5 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -483,6 +483,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
@ -504,6 +505,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
@ -931,6 +934,8 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
@ -941,9 +946,12 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
@ -1127,6 +1135,12 @@ github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:
|
||||
github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||
github.com/rhysd/actionlint v1.6.7 h1:cOojXJ/N3OAoOaTo8zEEA6uWovUC+0yP3dLYEZnRWg4=
|
||||
github.com/rhysd/actionlint v1.6.7/go.mod h1:vFbgcUNEK84kr9Lb3vEfqjTov/Q3X5py+ppKihWgH+w=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
Loading…
Reference in New Issue
Block a user