Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7fedff2
actions: stop unpinned-tag flagging $/ self-references
nodeselector Jul 10, 2026
180c109
actions: scaffold lockfile-aware pinning for unpinned-tag
nodeselector Jul 10, 2026
e80ae35
actions: add lockfile-to-data-extension generator for unpinned-tag
nodeselector Jul 10, 2026
9f8032d
actions: generate lockfile-pinned data extension during extraction
nodeselector Jul 10, 2026
9e7133b
actions: drop machine-specific replace, document lockfile generator l…
nodeselector Jul 10, 2026
d425c27
actions: keep lockfile generator go.mod free of local replace
nodeselector Jul 10, 2026
feed864
actions: rename self-reference to self repository in unpinned-tag
nodeselector Jul 10, 2026
c17aea9
actions: make lockfile-extension generation atomic
nodeselector Jul 10, 2026
287a59c
actions: drop private actions-lockfile dependency from generator
nodeselector Jul 10, 2026
b6a8394
actions: emit empty data list for zero-row lockfile extension
nodeselector Jul 10, 2026
f8f8aa8
actions/unpinned-tag: match lockfile pins case-insensitively on owner…
nodeselector Jul 13, 2026
2db34d2
actions: use US spelling in lockfile-aware pinning comments
nodeselector Jul 13, 2026
77ac6d9
actions: drop lockfile-extension generator from unpinned-tag PR
nodeselector Jul 23, 2026
91faa26
actions/unpinned-tag: normalize lockfile action identity
nodeselector Aug 31, 2026
45a6dd7
actions/unpinned-tag: regenerate expected results after sync
nodeselector Aug 31, 2026
2e04003
Actions: consume extracted lockfile pins
nodeselector Sep 2, 2026
24a18e4
Actions tests: add lockfile inline expectations
nodeselector Sep 2, 2026
e005d15
Actions tests: share inline expectations utility
nodeselector Sep 2, 2026
38f59df
Actions tests: keep inline adapter outside library API
nodeselector Sep 3, 2026
2b7e7ab
Actions lockfiles: ignore schema version value
nodeselector Sep 3, 2026
a2213ae
Actions lockfiles: match repository casing
nodeselector Sep 3, 2026
ff9bf5d
Code review suggestions
hvitved Sep 3, 2026
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
40 changes: 37 additions & 3 deletions actions/ql/lib/codeql/actions/Lock.qll
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,43 @@
* Provides classes for working with GitHub Actions lockfiles.
*/

private import actions
private import codeql.actions.ast.internal.Yaml

/** An `actions.lock` file. */
class ActionsLock extends YamlDocument {
ActionsLock() { this.getFile().getBaseName() = "actions.lock" }
/** A `.github/workflows/actions.lock` file. */
class ActionsLock extends YamlDocument, YamlMapping {
ActionsLock() { this.getFile().getRelativePath() = ".github/workflows/actions.lock" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because of this change, actions/ql/test/library-tests/actions-lock/actions.lock needs to be moved into actions/ql/test/library-tests/actions-lock/.github/workflows/actions.lock, and actions/ql/test/library-tests/actions-lock/test.expected needs to be updated accordingly.


private predicate pins0(string workflowPath, string pinnedNwo, string ref) {
exists(YamlSequence workflowPins, YamlScalar pinNode, YamlMapping dependency, string pin |
this.lookup("workflows").(YamlMapping).lookup(workflowPath) = workflowPins and
workflowPins.getElement(_) = pinNode and
pin = pinNode.getValue() and
pinnedNwo = pin.regexpCapture("^([^/@:]+/[^/@:]+)@([^:]+)$", 1) and
ref = pin.regexpCapture("^([^/@:]+/[^/@:]+)@([^:]+)$", 2) and
this.lookup("dependencies").(YamlMapping).lookup(pin) = dependency and
dependency.lookup("ref").(YamlScalar).getValue() = ref and
dependency
.lookup("commit")
.(YamlScalar)
.getValue()
.regexpMatch("^(sha1-[A-Fa-f0-9]{40}|sha256-[A-Fa-f0-9]{64})$")
)
}

/**
* Holds if this lockfile pins the use at `uses` to `ref` with a full commit digest.
* Repository pins also cover sub-actions such as `actions/cache/save`.
*/
predicate pins(UsesStep uses, string ref) {
exists(string workflowPath, string pinnedNwo, string nwo |
this.pins0(workflowPath, pinnedNwo, ref) and
workflowPath = uses.getLocation().getFile().getRelativePath() and
nwo = uses.getCallee()
|
nwo.toLowerCase() = pinnedNwo.toLowerCase()
or
nwo.toLowerCase().prefix(pinnedNwo.length() + 1) = pinnedNwo.toLowerCase() + "/"
)
}
}
8 changes: 8 additions & 0 deletions actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ private predicate isPinnedContainer(string version) {
bindingset[nwo]
private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+") }

// A `$/` reference is a same-repository (self repository) reference (e.g. `$/path/to/action`),
// resolved at the commit the calling workflow is running. Like `./` local (self workspace)
// references, it is inherently pinned and can never be an unpinned-tag finding, so we never flag it.
bindingset[nwo]
private predicate isSelfRepository(string nwo) { nwo.matches("$/%") }

private predicate hasUsesContainerName(Uses uses, string name) {
exists(Workflow workflow |
uses.getEnclosingWorkflow() = workflow and
Expand All @@ -55,6 +61,8 @@ where
hasUsesContainerName(uses, name) and
uses.getVersion() = version and
not isTrustedOwner(nwo) and
not isSelfRepository(nwo) and
not any(ActionsLock lock).pins(uses, version) and
not (
if uses instanceof UsesStep and isContainerImage(nwo)
then isPinnedContainer(version)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `actions/unpinned-tag` query no longer reports action references pinned by a structurally valid `.github/workflows/actions.lock` entry for the enclosing workflow.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `actions/unpinned-tag` query no longer reports `$/` self repository references (e.g. `uses: $/path/to/action`), which resolve to the same repository at the running commit and are therefore inherently pinned, just like `./` self workspace (local) references.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: future-version
workflows:
.github/workflows/rust-ci.yml:
- DToLnAy/RuSt-ToOlChAiN@v1
- mismatched/action@v1
- malformed/action@v1
- missing/action@v1
.github/workflows/other.yml:
- other-workflow/action@v1
dependencies:
DToLnAy/RuSt-ToOlChAiN@v1:
ref: v1
commit: sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772
owner_id: 1940490
repo_id: 260749683
other-workflow/action@v1:
ref: v1
commit: sha1-1111111111111111111111111111111111111111
owner_id: 1
repo_id: 2
mismatched/action@v1:
ref: V1
commit: sha1-2222222222222222222222222222222222222222
owner_id: 3
repo_id: 4
malformed/action@v1:
ref: v1
commit: 6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772
owner_id: 5
repo_id: 6
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
on:
pull_request

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@v1
- uses: DToLnAy/RuSt-ToOlChAiN/save@v1
- uses: dtolnay/rust-toolchain@V1 # $ Alert
- uses: other-workflow/action@v1 # $ Alert
- uses: mismatched/action@v1 # $ Alert
- uses: malformed/action@v1 # $ Alert
- uses: missing/action@v1 # $ Alert
reusable:
uses: dtolnay/rust-toolchain/.github/workflows/reusable.yml@v1 # $ Alert
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
| .github/workflows/rust-ci.yml:10:13:10:37 | dtolnay/rust-toolchain@V1 | Unpinned 3rd party Action 'rust-ci.yml' step $@ uses 'dtolnay/rust-toolchain' with ref 'V1', not a pinned commit hash | .github/workflows/rust-ci.yml:10:7:11:4 | Uses Step | Uses Step |
| .github/workflows/rust-ci.yml:11:13:11:36 | other-workflow/action@v1 | Unpinned 3rd party Action 'rust-ci.yml' step $@ uses 'other-workflow/action' with ref 'v1', not a pinned commit hash | .github/workflows/rust-ci.yml:11:7:12:4 | Uses Step | Uses Step |
| .github/workflows/rust-ci.yml:12:13:12:32 | mismatched/action@v1 | Unpinned 3rd party Action 'rust-ci.yml' step $@ uses 'mismatched/action' with ref 'v1', not a pinned commit hash | .github/workflows/rust-ci.yml:12:7:13:4 | Uses Step | Uses Step |
| .github/workflows/rust-ci.yml:13:13:13:31 | malformed/action@v1 | Unpinned 3rd party Action 'rust-ci.yml' step $@ uses 'malformed/action' with ref 'v1', not a pinned commit hash | .github/workflows/rust-ci.yml:13:7:14:4 | Uses Step | Uses Step |
| .github/workflows/rust-ci.yml:14:13:14:29 | missing/action@v1 | Unpinned 3rd party Action 'rust-ci.yml' step $@ uses 'missing/action' with ref 'v1', not a pinned commit hash | .github/workflows/rust-ci.yml:14:7:15:2 | Uses Step | Uses Step |
| .github/workflows/rust-ci.yml:16:11:16:66 | dtolnay/rust-toolchain/.github/workflows/reusable.yml@v1 | Job $@ in 'rust-ci.yml' uses reusable workflow 'dtolnay/rust-toolchain/.github/workflows/reusable.yml' with ref 'v1', not a pinned commit hash | .github/workflows/rust-ci.yml:16:5:16:77 | Job: reusable | Job: reusable |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: Security/CWE-829/UnpinnedActionsTag.ql
postprocess: utils/ActionsInlineExpectationsTestQuery.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
semmle-extractor-options: --file-type YAML .github/workflows/actions.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
on:
pull_request

jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
# `$/` is a same-repository (self repository) reference resolved at the running commit. It is
# inherently pinned (like `./` self workspace refs) and must never be reported as an unpinned tag.
- uses: $/actions/foo
# `$/…@ref` is rejected by the `$/` rule, but a user could still write it. It must also
# never be flagged; this case exercises the `not isSelfRepository(nwo)` suppression, since
# without it `$/actions/foo@v1` would otherwise be reported as an unpinned tag.
- uses: $/actions/foo@v1
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ edges
| .github/workflows/resolve-args.yml:20:9:22:6 | Uses Step | .github/workflows/resolve-args.yml:22:9:36:13 | Run Step: resolve-step |
| .github/workflows/reusable_local.yml:23:9:26:6 | Uses Step | .github/workflows/reusable_local.yml:26:9:29:7 | Run Step |
| .github/workflows/reusable_local.yml:25:17:25:36 | inputs.branch | .github/workflows/reusable_local.yml:23:9:26:6 | Uses Step |
| .github/workflows/self_ref_dollar.yml:11:7:15:4 | Uses Step | .github/workflows/self_ref_dollar.yml:15:7:15:29 | Uses Step |
| .github/workflows/test1.yml:18:9:21:6 | Uses Step | .github/workflows/test1.yml:21:9:24:6 | Run Step |
| .github/workflows/test1.yml:21:9:24:6 | Run Step | .github/workflows/test1.yml:24:9:25:39 | Run Step |
| .github/workflows/test2.yml:13:9:16:6 | Uses Step | .github/workflows/test2.yml:16:9:20:52 | Uses Step |
Expand Down
30 changes: 30 additions & 0 deletions actions/ql/test/utils/ActionsInlineExpectationsTestQuery.ql
Comment thread
nodeselector marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @kind test-postprocess
*/

private import codeql.Locations as Locations
private import codeql.actions.ast.internal.Yaml as Yaml
private import codeql.util.test.InlineExpectationsTest as T
import T::TestPostProcessing

private module Impl implements T::InlineExpectationsTestSig {
class Location = Locations::Location;

class ExpectationComment extends Yaml::YamlComment {
string getContents() { result = this.getText() }
}
}

private module Input implements T::TestPostProcessing::InputSig<Impl> {
string getRelativeUrl(Locations::Location location) {
exists(int startLine, int startColumn, int endLine, int endColumn |
location.hasLocationInfo(_, startLine, startColumn, endLine, endColumn)
|
result =
location.getFile().getRelativePath() + ":" + startLine + ":" + startColumn + ":" + endLine +
":" + endColumn
)
}
}

import T::TestPostProcessing::Make<Impl, Input>
Loading