From 735315012e76e633250506953138db1d89c30cb3 Mon Sep 17 00:00:00 2001 From: Sam Mortenson Date: Mon, 24 Jun 2024 13:18:08 -0400 Subject: [PATCH 1/2] Add optional lint to require that actions are pinned to commit hashes. --- config.go | 2 ++ linter_test.go | 4 ++++ rule_action.go | 15 +++++++++++++++ .../examples/invalid_action_format_security.out | 2 ++ .../examples/invalid_action_format_security.yaml | 11 +++++++++++ 5 files changed, 34 insertions(+) create mode 100644 testdata/examples/invalid_action_format_security.out create mode 100644 testdata/examples/invalid_action_format_security.yaml diff --git a/config.go b/config.go index 0a9821fa7..a9f09031e 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,8 @@ type Config struct { // listed here as undefined config variables. // https://docs.github.com/en/actions/learn-github-actions/variables ConfigVariables []string `yaml:"config-variables"` + // Requires action and docker versions to use a commit hash instead of version/branch. + RequireCommitHash bool `yaml:"require-commit-hash"` } func parseConfig(b []byte, path string) (*Config, error) { diff --git a/linter_test.go b/linter_test.go index 09e2226eb..f1360696c 100644 --- a/linter_test.go +++ b/linter_test.go @@ -188,6 +188,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 51e7f512f..e88e4e833 100644 --- a/rule_action.go +++ b/rule_action.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strconv" "strings" ) @@ -284,6 +285,8 @@ var BrandingIcons = map[string]struct{}{ "zoom-out": {}, } +var hashRegex = regexp.MustCompile("^[0-9a-f]{40}$") + // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsimage func isImageOnDockerRegistry(image string) bool { return strings.HasPrefix(image, "docker://") || @@ -367,6 +370,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 SHA1 hash") + } + meta, ok := PopularActions[spec] if !ok { if _, ok := OutdatedPopularActionSpecs[spec]; ok { @@ -390,6 +397,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) } @@ -518,6 +529,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 && !hashRegex.MatchString(tag) { + rule.Errorf(exec.Uses.Pos, "docker versions must be pinned to SHA1 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..6b42db858 --- /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 SHA1 hash. available formats are "{owner}/{repo}@{sha}" or "{owner}/{repo}/{path}@{sha}" [action] +test.yaml:9:15: docker versions must be pinned to SHA1 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..d84a1dc87 --- /dev/null +++ b/testdata/examples/invalid_action_format_security.yaml @@ -0,0 +1,11 @@ +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 From cb07522ad3bd351327b69ebf02f885f9cf4605e1 Mon Sep 17 00:00:00 2001 From: Sam Mortenson Date: Mon, 24 Jun 2024 14:13:18 -0400 Subject: [PATCH 2/2] Added docs --- README.md | 4 ++-- docs/checks.md | 7 +++++++ docs/config.md | 5 ++++- rule_action.go | 8 +++++--- testdata/examples/invalid_action_format_security.out | 4 ++-- testdata/examples/invalid_action_format_security.yaml | 2 ++ 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1966b16b2..19e726456 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ See [the usage document](docs/usage.md) for more details. source, a download script (for CI), supports by several package managers are available. - [Usage](docs/usage.md): How to use `actionlint` command locally or on GitHub Actions, the online playground, an official Docker image, and integrations with reviewdog, Problem Matchers, super-linter, pre-commit, VS Code. -- [Configuration](docs/config.md): How to configure actionlint behavior. Currently, the labels of self-hosted runners and the - configuration variables can be set. +- [Configuration](docs/config.md): How to configure actionlint behavior. Currently, the labels of self-hosted runners, + configuration variables, and optional security lints can be set. - [Go API](docs/api.md): How to use actionlint as Go library. - [References](docs/reference.md): Links to resources. diff --git a/docs/checks.md b/docs/checks.md index daa7a9a68..971c4fc0e 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -1646,6 +1646,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: @@ -1667,6 +1670,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#eJxdzTEOgzAMBdCdU3hjSi119NSrJKlFUkqMsF2pty8UsWTy139fsjSC1bUML0lKA4Cx2nEBNm8aZHdP3szDOx72JzVe9VwBBHBlJYjZqjTFXDjP4tbxVT8+907Gp+SZN0KsS5yYxs5vOFUrnvD6sHzDGX98DjoH) diff --git a/docs/config.md b/docs/config.md index 2d5775f0f..59d35a8c1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -21,7 +21,7 @@ actionlint -init-config vim .github/actionlint.yaml ``` -Currently only one item can be configured. +Here are the items that can be configured: ```yaml self-hosted-runner: @@ -35,6 +35,8 @@ config-variables: - DEFAULT_RUNNER - JOB_NAME - ENVIRONMENT_STAGE +# 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. @@ -42,6 +44,7 @@ config-variables: is available. - `config-variables`: [Configuration variables][vars]. When an array is set, actionlint will check `vars` properties strictly. An empty array means no variable is allowed. The default value `null` disables the check. +- `require-commit-hash`: Optional lint to require actions to be pinned to commit hashes instead of tags/branches. Defaults to `false`. --- diff --git a/rule_action.go b/rule_action.go index e88e4e833..7eefc1332 100644 --- a/rule_action.go +++ b/rule_action.go @@ -287,6 +287,8 @@ var BrandingIcons = map[string]struct{}{ 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,7 +373,7 @@ func (rule *RuleAction) checkRepoAction(spec string, exec *ExecAction) { } if rule.config != nil && rule.config.RequireCommitHash && !hashRegex.MatchString(ref) { - rule.invalidActionFormatCommitHash(exec.Uses.Pos, spec, "action versions must be pinned to SHA1 hash") + rule.invalidActionFormatCommitHash(exec.Uses.Pos, spec, "action versions must be pinned to a SHA hash") } meta, ok := PopularActions[spec] @@ -530,8 +532,8 @@ func (rule *RuleAction) checkDockerAction(uri string, exec *ExecAction) { rule.Errorf(exec.Uses.Pos, "tag of Docker action should not be empty: %q", uri) } - if rule.config != nil && rule.config.RequireCommitHash && !hashRegex.MatchString(tag) { - rule.Errorf(exec.Uses.Pos, "docker versions must be pinned to SHA1 hash: %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) } } diff --git a/testdata/examples/invalid_action_format_security.out b/testdata/examples/invalid_action_format_security.out index 6b42db858..3bc111aa0 100644 --- a/testdata/examples/invalid_action_format_security.out +++ b/testdata/examples/invalid_action_format_security.out @@ -1,2 +1,2 @@ -test.yaml:7:15: specifying action "actions/checkout@main" in invalid format because action versions must be pinned to SHA1 hash. available formats are "{owner}/{repo}@{sha}" or "{owner}/{repo}/{path}@{sha}" [action] -test.yaml:9:15: docker versions must be pinned to SHA1 hash: "docker://image" [action] +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 index d84a1dc87..3c5219b2c 100644 --- a/testdata/examples/invalid_action_format_security.yaml +++ b/testdata/examples/invalid_action_format_security.yaml @@ -9,3 +9,5 @@ jobs: - 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