Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional lint to require that actions are pinned to commit hashes #436

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -35,13 +35,16 @@ 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.
- `labels`: Label names added to your self-hosted runners as list of pattern. Glob syntax supported by [`path.Match`][pat]
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`.

---

Expand Down
4 changes: 4 additions & 0 deletions linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions rule_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
Expand Down Expand Up @@ -284,6 +285,10 @@ var BrandingIcons = map[string]struct{}{
"zoom-out": {},
}

var hashRegex = regexp.MustCompile("^[0-9a-f]{40}$")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use short hashes as well, but I was almost certain that would conflict with branch names so this seems better.


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://") ||
Expand Down Expand Up @@ -367,6 +372,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 {
Expand All @@ -390,6 +399,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)
}
Expand Down Expand Up @@ -518,6 +531,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
Expand Down
2 changes: 2 additions & 0 deletions testdata/examples/invalid_action_format_security.out
Original file line number Diff line number Diff line change
@@ -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]
13 changes: 13 additions & 0 deletions testdata/examples/invalid_action_format_security.yaml
Original file line number Diff line number Diff line change
@@ -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