Skip to content

v2.1.0: JACTIONLINT_ONLINE, workflow-secret-scope, and more precise security rules

Latest

Choose a tag to compare

@jdx jdx released this 10 Oct 13:59
Immutable release. Only release title and notes can be modified.
a0bb9f0

jactionlint 2.1.0 adds an environment variable that turns on the online checks, a new workflow-secret-scope rule, and machine-readable reasons when a finding has no fix. Many security rules now produce fewer false positives, especially in reusable workflows and release pipelines. Some rules now report things they did not report before, so an existing clean run can show new findings (see Changed).

Added

  • JACTIONLINT_ONLINE environment variable. It turns on the online checks without changing the command, so CI can run them while local pre-commit hooks stay offline. 1, true or on works like --online, and cache, strict or cache,strict select a mode. 0, false, off or no works like --no-online and overrides online: true in the config file. The --online/--no-online flags override the variable, and the variable overrides the config file. An invalid value exits with status 2. (#155, @jdx)
    - run: hk check --all
      env:
        JACTIONLINT_ONLINE: 1
        GITHUB_TOKEN: ${{ github.token }}
  • New workflow-secret-scope rule (security, pedantic profile, warning, no autofix). It reports a named secret in workflow-level env: when steps in two or more jobs can see it. The rule skips single-job workflows, GITHUB_TOKEN, jobs or steps that override the variable, and jobs that call reusable workflows. (#136, @jdx)
  • background-step-not-waited (correctness profile, from parallel-steps). It reports reads of a background step's outputs, outcome or conclusion before a wait or wait-all covers that step. Those reads evaluate to an empty string. Steps with background: ${{ ... }} are not tracked. (#132, @jdx)
  • Reasons for missing fixes. When a fixable rule has no fix for a finding, the finding now says why. This applies to missing-timeout, dependabot-cooldown, unpinned-uses, template-injection and artipacked. The reason appears as no_fix in JSON and template output, and as noFixCode, noFixReason and noFixOption properties in SARIF. The codes are option-required, option-invalid, online-required, lookup-failed, unsupported-shape and needs-judgment. Text output is unchanged. When default-minutes or default-days is not set, the missing-timeout and dependabot-cooldown messages now say which option to set to enable --fix. (#135, #147, #154, @jdx)
  • Online check coverage in SARIF. When online checks are on, SARIF runs include properties.onlineCoverage with status (complete/incomplete), mode, skippedLookups, excludedRepositories and evidence (network or cache). (#140, @jdx)

Changed

  • excessive-permissions now reports workflow-level write scopes in single-job workflows too, unless the trigger is privileged. Any job added later would inherit those scopes without notice. (#152, @jdx)

  • missing-permissions no longer reports workflows whose only trigger is workflow_call, because the callers decide the token permissions. Workflows that have workflow_call plus another trigger are still reported. They now get an unsafe fix (contents: read, applied only with --fix=unsafe), and the message says the permissions must not exceed what callers grant. (#138, #152, @jdx)

  • Parallel-step control keys now match what GitHub accepts. On wait, wait-all and cancel steps, only id and name are allowed besides the keyword. On parallel steps, only parallel is allowed. wait-all: false is now an error. To apply a condition, put if: on a step inside the parallel group instead of on the group. (#128, @navidemad)

  • concurrency-limits:

    • Workflows that call reusable workflows now get a caller-specific concurrency group, so the caller's group no longer collides with the called workflow's group.
    • Release detection now recognizes many more publish commands (for example semantic-release, changeset publish, mvn deploy, cargo release --execute), tag pushes and release actions. Release jobs get cancel-in-progress: false advice and no fix.
    • --fix now inserts a blank line before the concurrency: or permissions: block it adds.

    (#142, @jdx)

  • bot-conditions now uses the events of a reusable workflow's local callers. Workflows that only push-like events start are no longer reported. Mixed or non-PR events get advice that fits those events, and only PR-only workflows keep the unsafe fix. (#141, @jdx)

  • pipeline-without-pipefail: copying a stage's exit code into a variable (rc=${PIPESTATUS[0]}) now counts as handling the pipeline only if a later [/[[/test, (( )), exit or return checks that variable. (#145, @jdx)

  • untrusted-checkout: after a step fetches a pull request revision, a later step that runs a repository script (such as ./scripts/x.sh or bash scripts/x.sh) is now reported if that script runs a work-tree git command on FETCH_HEAD or the fetch destination. The rule only follows static, in-repo script paths. (#149, @jdx)

  • cache-poisoning: a push whose branches: filter lists only release branches (such as release/**) now counts as a release trigger. The built-in list of publishing actions also gained more entries, including crates.io auth, Homebrew bump actions, Azure/GCP/AWS deploys, Vercel, Netlify, Google Play and TestFlight. (#150, @jdx)

  • On a first run of the default profile with 20 or more findings, the note on stderr now starts by suggesting --baseline-write. (#152, @jdx)

Fixed

  • runs.env is no longer rejected in composite actions. JavaScript actions still reject it. (#123, @jdx)

  • The issues event now accepts the field_added and field_removed activity types. (#130, @jdx)

  • background: true on a step inside a parallel group is no longer reported. It is redundant, but GitHub accepts it. (#131, @jdx)

  • pipeline-without-pipefail no longer reports pipelines inside process substitutions (< <(...), > >(...)). (#143, @jdx)

  • cache-poisoning now evaluates expressions that callers pass to a composite action's cache-switch input (for example cache: ${{ !startsWith(github.ref, 'refs/tags/') }}) for each release scenario. Before, only literal values were evaluated. (#139, @jdx)

  • use-trusted-publishing: a scoped registry (@scope:registry) now hides a publish only when the package, read from package.json, belongs to that scope. (#151, @jdx)

  • github-env: a guard inside an if/else branch now protects only the writes later in that same branch. (#151, @jdx)

  • template-injection: --fix=unsafe no longer quotes unquoted expressions where the shell may split words, such as command arguments, redirections, word lists and arrays. These findings now have no fix and a needs-judgment reason. Assignment values (NAME=${{ x }}) are still fixed. (#154, @jdx)

  • artipacked: --fix now also fixes flow-style checkout steps (- {uses: ...}) and multi-line flow with: mappings. (#154, @jdx)

  • --migrate-ignores:

    • With no file arguments, it now also migrates the root action.yml/action.yaml.
    • Each comment it skips is listed as file:line: not migrated: <reason>.
    • A version after a zizmor comment on a uses: line (# zizmor: ignore[artipacked] v6) now stays on that line as # v6.

    (#146, @jdx)

  • Linting allocates about 9 to 10% less memory, because each ${{ }} expression is now parsed only once and the expression lexer no longer allocates a scanner. (#153, @jdx)

New Contributors

Full Changelog: v2.0.2...v2.1.0

💚 Sponsor jactionlint

jactionlint is a fork of rhysd/actionlint, maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.

If jactionlint has a place in your development workflow, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep jactionlint fast, free, and independent.