mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-17 11:57:12 +03:00
Moving github worflow parsing to its own file
This commit is contained in:
parent
b3ac52a06b
commit
f319aca82d
220
checks/fileparser/github_workflow.go
Normal file
220
checks/fileparser/github_workflow.go
Normal file
@ -0,0 +1,220 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
sce "github.com/ossf/scorecard/v3/errors"
|
||||
)
|
||||
|
||||
// defaultShellNonWindows is the default shell used for GitHub workflow actions for Linux and Mac.
|
||||
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
|
||||
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))
|
||||
}
|
||||
*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
|
||||
}
|
||||
|
||||
// GetOSesForJob returns the OSes this job runs on.
|
||||
func GetOSesForJob(job *GitHubActionWorkflowJob) ([]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
|
||||
}
|
||||
}
|
||||
}
|
||||
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to determine OS for job: %v", job.Name))
|
||||
}
|
||||
|
||||
// JobAlwaysRunsOnWindows returns true if the only OS that this job runs on is Windows.
|
||||
func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) {
|
||||
jobOSes, err := GetOSesForJob(job)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, os := range jobOSes {
|
||||
if !strings.HasPrefix(strings.ToLower(os), "windows") {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetShellForStep returns the shell that is used to run the given step.
|
||||
func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJob) (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
|
||||
}
|
||||
if job.Defaults.Run.Shell != "" {
|
||||
return job.Defaults.Run.Shell, nil
|
||||
}
|
||||
|
||||
isStepWindows, err := IsStepWindows(step)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isStepWindows {
|
||||
return defaultShellWindows, nil
|
||||
}
|
||||
|
||||
alwaysRunsOnWindows, err := JobAlwaysRunsOnWindows(job)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if alwaysRunsOnWindows {
|
||||
return defaultShellWindows, nil
|
||||
}
|
||||
|
||||
return defaultShellNonWindows, nil
|
||||
}
|
||||
|
||||
// IsStepWindows returns true if the step will be run on Windows.
|
||||
func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) {
|
||||
windowsRegexes := []string{
|
||||
// Looking for "if: runner.os == 'Windows'" (and variants)
|
||||
`(?i)runner\.os\s*==\s*['"]windows['"]`,
|
||||
// Looking for "if: ${{ startsWith(runner.os, 'Windows') }}" (and variants)
|
||||
`(?i)\$\{\{\s*startsWith\(runner\.os,\s*['"]windows['"]\)`,
|
||||
// Looking for "if: matrix.os == 'windows-2019'" (and variants)
|
||||
`(?i)matrix\.os\s*==\s*['"]windows-`,
|
||||
}
|
||||
|
||||
for _, windowsRegex := range windowsRegexes {
|
||||
matches, err := regexp.MatchString(windowsRegex, step.If)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err))
|
||||
}
|
||||
if matches {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsWorkflowFile returns true if this is a GitHub workflow file.
|
||||
func IsWorkflowFile(pathfn string) bool {
|
||||
// From https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions:
|
||||
// "Workflow files use YAML syntax, and must have either a .yml or .yaml file extension."
|
||||
switch path.Ext(pathfn) {
|
||||
case ".yml", ".yaml":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsGitHubOwnedAction checks if this is a github specific action.
|
||||
func IsGitHubOwnedAction(actionName string) bool {
|
||||
a := strings.HasPrefix(actionName, "actions/")
|
||||
c := strings.HasPrefix(actionName, "github/")
|
||||
return a || c
|
||||
}
|
138
checks/fileparser/github_workflow_test.go
Normal file
138
checks/fileparser/github_workflow_test.go
Normal file
@ -0,0 +1,138 @@
|
||||
// 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 (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
func TestGitHubWorkflowShell(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repeatItem := func(item string, count int) []string {
|
||||
ret := make([]string, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
// The shells used in each step, listed in order that the steps are listed in the file
|
||||
expectedShells []string
|
||||
}{
|
||||
{
|
||||
name: "all windows, shell specified in step",
|
||||
filename: "../testdata/github-workflow-shells-all-windows-bash.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "all windows, OSes listed in matrix.os",
|
||||
filename: "../testdata/github-workflow-shells-all-windows-matrix.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "all windows",
|
||||
filename: "../testdata/github-workflow-shells-all-windows.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "macOS defaults to bash",
|
||||
filename: "../testdata/github-workflow-shells-default-macos.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "ubuntu defaults to bash",
|
||||
filename: "../testdata/github-workflow-shells-default-ubuntu.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "windows defaults to pwsh",
|
||||
filename: "../testdata/github-workflow-shells-default-windows.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "windows specified in 'if'",
|
||||
filename: "../testdata/github-workflow-shells-runner-windows-ubuntu.yaml",
|
||||
expectedShells: append(repeatItem("pwsh", 7), repeatItem("bash", 4)...),
|
||||
},
|
||||
{
|
||||
name: "shell specified in job and step",
|
||||
filename: "../testdata/github-workflow-shells-specified-job-step.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "windows, shell specified in job",
|
||||
filename: "../testdata/github-workflow-shells-specified-job-windows.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "shell specified in job",
|
||||
filename: "../testdata/github-workflow-shells-specified-job.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "shell specified in step",
|
||||
filename: "../testdata/github-workflow-shells-speficied-step.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "different shells in each step",
|
||||
filename: "../testdata/github-workflow-shells-two-shells.yaml",
|
||||
expectedShells: []string{"bash", "pwsh"},
|
||||
},
|
||||
{
|
||||
name: "windows step, bash specified",
|
||||
filename: "../testdata/github-workflow-shells-windows-bash.yaml",
|
||||
expectedShells: []string{"bash", "bash"},
|
||||
},
|
||||
}
|
||||
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 := ioutil.ReadFile(tt.filename)
|
||||
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)
|
||||
}
|
||||
actualShells := make([]string, 0)
|
||||
for _, job := range workflow.Jobs {
|
||||
job := job
|
||||
for _, step := range job.Steps {
|
||||
step := step
|
||||
shell, err := GetShellForStep(&step, &job)
|
||||
if err != nil {
|
||||
t.Errorf("error getting shell: %v", err)
|
||||
}
|
||||
actualShells = append(actualShells, shell)
|
||||
}
|
||||
}
|
||||
if !cmp.DeepEqual(tt.expectedShells, actualShells)().Success() {
|
||||
t.Errorf("%v: Got (%v) expected (%v)", tt.name, actualShells, tt.expectedShells)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import (
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/ossf/scorecard/v3/checker"
|
||||
"github.com/ossf/scorecard/v3/checks/fileparser"
|
||||
sce "github.com/ossf/scorecard/v3/errors"
|
||||
)
|
||||
|
||||
@ -360,7 +361,7 @@ func testValidateGitHubActionTokenPermissions(pathfn string,
|
||||
// Check file content.
|
||||
func validateGitHubActionTokenPermissions(path string, content []byte,
|
||||
dl checker.DetailLogger, data FileCbData) (bool, error) {
|
||||
if !isWorkflowFile(path) {
|
||||
if !fileparser.IsWorkflowFile(path) {
|
||||
return true, nil
|
||||
}
|
||||
// Verify the type of the data.
|
||||
|
@ -16,7 +16,6 @@ package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -24,84 +23,13 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/ossf/scorecard/v3/checker"
|
||||
"github.com/ossf/scorecard/v3/checks/fileparser"
|
||||
sce "github.com/ossf/scorecard/v3/errors"
|
||||
)
|
||||
|
||||
// CheckPinnedDependencies is the registered name for FrozenDeps.
|
||||
const CheckPinnedDependencies = "Pinned-Dependencies"
|
||||
|
||||
// defaultShellNonWindows is the default shell used for GitHub workflow actions for Linux and Mac.
|
||||
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
|
||||
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))
|
||||
}
|
||||
*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
|
||||
}
|
||||
|
||||
// Structure to host information about pinned github
|
||||
// or third party dependencies.
|
||||
type worklowPinningResult struct {
|
||||
@ -109,16 +37,6 @@ type worklowPinningResult struct {
|
||||
gitHubOwned pinnedResult
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
registerCheck(CheckPinnedDependencies, PinnedDependencies)
|
||||
@ -522,7 +440,7 @@ func testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string,
|
||||
// Returns true if the check should continue executing after this file.
|
||||
func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
||||
dl checker.DetailLogger, data FileCbData) (bool, error) {
|
||||
if !isWorkflowFile(pathfn) {
|
||||
if !fileparser.IsWorkflowFile(pathfn) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@ -533,7 +451,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var workflow gitHubActionWorkflowConfig
|
||||
var workflow fileparser.GitHubActionWorkflowConfig
|
||||
err := yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
@ -552,7 +470,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
|
||||
shell, err := getShellForStep(&step, &job)
|
||||
shell, err := fileparser.GetShellForStep(&step, &job)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -579,99 +497,6 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Returns the OSes this job runs on.
|
||||
func getOSesForJob(job *gitHubActionWorkflowJob) ([]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
|
||||
}
|
||||
}
|
||||
}
|
||||
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
fmt.Sprintf("unable to determine OS for job: %v", job.Name))
|
||||
}
|
||||
|
||||
// The only OS that this job runs on is Windows.
|
||||
func jobAlwaysRunsOnWindows(job *gitHubActionWorkflowJob) (bool, error) {
|
||||
jobOSes, err := getOSesForJob(job)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, os := range jobOSes {
|
||||
if !strings.HasPrefix(strings.ToLower(os), "windows") {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getShellForStep returns the shell that is used to run the given step.
|
||||
func getShellForStep(step *gitHubActionWorkflowStep, job *gitHubActionWorkflowJob) (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
|
||||
}
|
||||
if job.Defaults.Run.Shell != "" {
|
||||
return job.Defaults.Run.Shell, nil
|
||||
}
|
||||
|
||||
isStepWindows, err := isStepWindows(step)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isStepWindows {
|
||||
return defaultShellWindows, nil
|
||||
}
|
||||
|
||||
alwaysRunsOnWindows, err := jobAlwaysRunsOnWindows(job)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if alwaysRunsOnWindows {
|
||||
return defaultShellWindows, nil
|
||||
}
|
||||
|
||||
return defaultShellNonWindows, nil
|
||||
}
|
||||
|
||||
// isStepWindows returns true if the step will be run on Windows.
|
||||
func isStepWindows(step *gitHubActionWorkflowStep) (bool, error) {
|
||||
windowsRegexes := []string{
|
||||
// Looking for "if: runner.os == 'Windows'" (and variants)
|
||||
`(?i)runner\.os\s*==\s*['"]windows['"]`,
|
||||
// Looking for "if: ${{ startsWith(runner.os, 'Windows') }}" (and variants)
|
||||
`(?i)\$\{\{\s*startsWith\(runner\.os,\s*['"]windows['"]\)`,
|
||||
// Looking for "if: matrix.os == 'windows-2019'" (and variants)
|
||||
`(?i)matrix\.os\s*==\s*['"]windows-`,
|
||||
}
|
||||
|
||||
for _, windowsRegex := range windowsRegexes {
|
||||
matches, err := regexp.MatchString(windowsRegex, step.If)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err))
|
||||
}
|
||||
if matches {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check pinning of github actions in workflows.
|
||||
func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) {
|
||||
var r worklowPinningResult
|
||||
@ -697,7 +522,7 @@ func testIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker
|
||||
// should continue executing after this file.
|
||||
func validateGitHubActionWorkflow(pathfn string, content []byte,
|
||||
dl checker.DetailLogger, data FileCbData) (bool, error) {
|
||||
if !isWorkflowFile(pathfn) {
|
||||
if !fileparser.IsWorkflowFile(pathfn) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@ -709,7 +534,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var workflow gitHubActionWorkflowConfig
|
||||
var workflow fileparser.GitHubActionWorkflowConfig
|
||||
err := yaml.Unmarshal(content, &workflow)
|
||||
if err != nil {
|
||||
return false, sce.WithMessage(sce.ErrScorecardInternal,
|
||||
@ -733,34 +558,14 @@ func validateGitHubActionWorkflow(pathfn string, content []byte,
|
||||
})
|
||||
}
|
||||
|
||||
githubOwned := isGitHubOwnedAction(step.Uses.Value)
|
||||
addWorkflowPinnedResult(pdata, match, githubOwned)
|
||||
}
|
||||
githubOwned := fileparser.IsGitHubOwnedAction(execAction.Uses.Value)
|
||||
addWorkflowPinnedResult(pdata, match, githubOwned)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isWorkflowFile returns true if this is a GitHub workflow file.
|
||||
func isWorkflowFile(pathfn string) bool {
|
||||
// From https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions:
|
||||
// "Workflow files use YAML syntax, and must have either a .yml or .yaml file extension."
|
||||
switch path.Ext(pathfn) {
|
||||
case ".yml", ".yaml":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isGitHubOwnedAction check github specific action.
|
||||
func isGitHubOwnedAction(v string) bool {
|
||||
a := strings.HasPrefix(v, "actions/")
|
||||
c := strings.HasPrefix(v, "github/")
|
||||
return a || c
|
||||
}
|
||||
|
||||
func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) {
|
||||
if isGitHub {
|
||||
addPinnedResult(&w.gitHubOwned, to)
|
||||
|
@ -19,9 +19,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/ossf/scorecard/v3/checker"
|
||||
scut "github.com/ossf/scorecard/v3/utests"
|
||||
)
|
||||
@ -968,118 +965,3 @@ func TestGitHubWorkflowUsesLineNumber(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubWorkflowShell(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repeatItem := func(item string, count int) []string {
|
||||
ret := make([]string, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
// The shells used in each step, listed in order that the steps are listed in the file
|
||||
expectedShells []string
|
||||
}{
|
||||
{
|
||||
name: "all windows, shell specified in step",
|
||||
filename: "testdata/github-workflow-shells-all-windows-bash.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "all windows, OSes listed in matrix.os",
|
||||
filename: "testdata/github-workflow-shells-all-windows-matrix.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "all windows",
|
||||
filename: "testdata/github-workflow-shells-all-windows.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "macOS defaults to bash",
|
||||
filename: "testdata/github-workflow-shells-default-macos.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "ubuntu defaults to bash",
|
||||
filename: "testdata/github-workflow-shells-default-ubuntu.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "windows defaults to pwsh",
|
||||
filename: "testdata/github-workflow-shells-default-windows.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "windows specified in 'if'",
|
||||
filename: "testdata/github-workflow-shells-runner-windows-ubuntu.yaml",
|
||||
expectedShells: append(repeatItem("pwsh", 7), repeatItem("bash", 4)...),
|
||||
},
|
||||
{
|
||||
name: "shell specified in job and step",
|
||||
filename: "testdata/github-workflow-shells-specified-job-step.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "windows, shell specified in job",
|
||||
filename: "testdata/github-workflow-shells-specified-job-windows.yaml",
|
||||
expectedShells: []string{"bash"},
|
||||
},
|
||||
{
|
||||
name: "shell specified in job",
|
||||
filename: "testdata/github-workflow-shells-specified-job.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "shell specified in step",
|
||||
filename: "testdata/github-workflow-shells-speficied-step.yaml",
|
||||
expectedShells: []string{"pwsh"},
|
||||
},
|
||||
{
|
||||
name: "different shells in each step",
|
||||
filename: "testdata/github-workflow-shells-two-shells.yaml",
|
||||
expectedShells: []string{"bash", "pwsh"},
|
||||
},
|
||||
{
|
||||
name: "windows step, bash specified",
|
||||
filename: "testdata/github-workflow-shells-windows-bash.yaml",
|
||||
expectedShells: []string{"bash", "bash"},
|
||||
},
|
||||
}
|
||||
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 := ioutil.ReadFile(tt.filename)
|
||||
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)
|
||||
}
|
||||
actualShells := make([]string, 0)
|
||||
for _, job := range workflow.Jobs {
|
||||
job := job
|
||||
for _, step := range job.Steps {
|
||||
step := step
|
||||
shell, err := getShellForStep(&step, &job)
|
||||
if err != nil {
|
||||
t.Errorf("error getting shell: %v", err)
|
||||
}
|
||||
actualShells = append(actualShells, shell)
|
||||
}
|
||||
}
|
||||
if !cmp.Equal(tt.expectedShells, actualShells) {
|
||||
t.Errorf("%v: Got (%v) expected (%v)", tt.name, actualShells, tt.expectedShells)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user