diff --git a/config.go b/config.go index 74913f6d7..ea4dc1170 100644 --- a/config.go +++ b/config.go @@ -67,6 +67,8 @@ type Config struct { // Paths is a "paths" mapping in the configuration file. The keys are glob patterns to match file paths. // And the values are corresponding configurations applied to the file paths. Paths map[string]PathConfig `yaml:"paths"` + // Requires action and docker versions to use a commit hash instead of version/branch. + RequireCommitHash bool `yaml:"require-commit-hash"` } // PathConfigs returns a list of all PathConfig values matching to the given file path. The path must diff --git a/docs/checks.md b/docs/checks.md index 4a330af99..91edd70e8 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -1708,6 +1708,9 @@ jobs: - uses: 'docker://image:' # ERROR: local action must start with './' - uses: .github/my-actions/do-something + # Optional when `require-commit-hash: true` is set in .github/actionlint.yaml: + # ERROR: not pinned to commit hash + - uses: actions/checkout@main ``` Output: @@ -1729,6 +1732,10 @@ test.yaml:13:15: specifying action ".github/my-actions/do-something" in invalid | 13 | - uses: .github/my-actions/do-something | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +test.yaml:16:15: specifying action "actions/checkout@main" in invalid format because action versions must be pinned to a SHA hash. available formats are "{owner}/{repo}@{sha}" or "{owner}/{repo}/{path}@{sha}" [action] + | +16 | - uses: actions/checkout@main + | ^~~~~~~~~~~~~~~~~~~~~ ``` [Playground](https://rhysd.github.io/actionlint/#eNpczbEOwyAMBNA9X+EtE0XqyNRfAWIBTbGj2K7Uv69olYXppHsnHVOAw6QuT04SFgBF0ZEAp5G44ZaM1NwrDvuRKB7yXwE4MEEJELM2JvG5Yt7ZdOKrfrzvk6wb5x3P4H3rsWBYJ7+VptWS7x93fWzshDtqbVS+AQAA//+oTjwo) diff --git a/docs/config.md b/docs/config.md index 2a2fdc47f..337d30e80 100644 --- a/docs/config.md +++ b/docs/config.md @@ -42,6 +42,8 @@ paths: ignore: # Ignore errors from the old runner check. This may be useful for (outdated) self-hosted runner environment. - 'the runner of ".+" action is too old to run on GitHub Actions' +# Require actions to be pinned to commit hashes instead of tags/branches +require-commit-hash: true ``` - `self-hosted-runner`: Configuration for your self-hosted runner environment. @@ -57,6 +59,7 @@ paths: - `ignore`: The configuration to ignore (filter) the errors by the error messages. This is an array of regular expressions. When one of the patterns matches the error message, the error will be ignored. It's similar to the `-ignore` command line option. +- `require-commit-hash`: Optional lint to require actions to be pinned to commit hashes instead of tags/branches. Defaults to `false`. ## Generate the initial configuration diff --git a/linter_test.go b/linter_test.go index 8621c4c1a..aeeed0a6d 100644 --- a/linter_test.go +++ b/linter_test.go @@ -201,6 +201,10 @@ func TestLinterLintError(t *testing.T) { l.defaultConfig = &Config{} + if strings.Contains(testName, "security") { + l.defaultConfig.RequireCommitHash = true + } + errs, err := l.Lint("test.yaml", b, proj) if err != nil { t.Fatal(err) diff --git a/rule_action.go b/rule_action.go index 3cc181c64..54df09c61 100644 --- a/rule_action.go +++ b/rule_action.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strconv" "strings" ) @@ -286,6 +287,10 @@ var BrandingIcons = map[string]struct{}{ "zoom-out": {}, } +var hashRegex = regexp.MustCompile("^[0-9a-f]{40}$") + +var dockerDigestHashRegex = regexp.MustCompile("^sha256:") + // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsimage func isImageOnDockerRegistry(image string) bool { return strings.HasPrefix(image, "docker://") || @@ -371,6 +376,10 @@ func (rule *RuleAction) checkRepoAction(spec string, exec *ExecAction) { rule.invalidActionFormat(exec.Uses.Pos, spec, "owner and repo and ref should not be empty") } + if rule.config != nil && rule.config.RequireCommitHash && !hashRegex.MatchString(ref) { + rule.invalidActionFormatCommitHash(exec.Uses.Pos, spec, "action versions must be pinned to a SHA hash") + } + meta, ok := PopularActions[spec] if !ok { if _, ok := OutdatedPopularActionSpecs[spec]; ok { @@ -394,6 +403,10 @@ func (rule *RuleAction) invalidActionFormat(pos *Pos, spec string, why string) { rule.Errorf(pos, "specifying action %q in invalid format because %s. available formats are \"{owner}/{repo}@{ref}\" or \"{owner}/{repo}/{path}@{ref}\"", spec, why) } +func (rule *RuleAction) invalidActionFormatCommitHash(pos *Pos, spec string, why string) { + rule.Errorf(pos, "specifying action %q in invalid format because %s. available formats are \"{owner}/{repo}@{sha}\" or \"{owner}/{repo}/{path}@{sha}\"", spec, why) +} + func (rule *RuleAction) missingRunsProp(pos *Pos, prop, ty, action, path string) { rule.Errorf(pos, `%q is required in "runs" section because %q is a %s action. the action is defined at %q`, prop, action, ty, path) } @@ -522,6 +535,10 @@ func (rule *RuleAction) checkDockerAction(uri string, exec *ExecAction) { if tagExists && tag == "" { rule.Errorf(exec.Uses.Pos, "tag of Docker action should not be empty: %q", uri) } + + if rule.config != nil && rule.config.RequireCommitHash && !dockerDigestHashRegex.MatchString(tag) { + rule.Errorf(exec.Uses.Pos, "docker versions must be pinned to a SHA hash: %q", uri) + } } // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions diff --git a/testdata/examples/invalid_action_format_security.out b/testdata/examples/invalid_action_format_security.out new file mode 100644 index 000000000..3bc111aa0 --- /dev/null +++ b/testdata/examples/invalid_action_format_security.out @@ -0,0 +1,2 @@ +test.yaml:7:15: specifying action "actions/checkout@main" in invalid format because action versions must be pinned to a SHA hash. available formats are "{owner}/{repo}@{sha}" or "{owner}/{repo}/{path}@{sha}" [action] +test.yaml:9:15: docker versions must be pinned to a SHA hash: "docker://image" [action] diff --git a/testdata/examples/invalid_action_format_security.yaml b/testdata/examples/invalid_action_format_security.yaml new file mode 100644 index 000000000..3c5219b2c --- /dev/null +++ b/testdata/examples/invalid_action_format_security.yaml @@ -0,0 +1,13 @@ +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + # ERROR: not pinned to commit hash + - uses: actions/checkout@main + # ERROR: docker image not pinned to commit hash + - uses: 'docker://image:latest' + # OK: pinned to commit hash + - uses: actions/checkout@db41740e12847bb616a339b75eb9414e711417df + # OK: docker image pinned to commit hash + - uses: docker://image:sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f