Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fd23d39
actions: stop unpinned-tag flagging $/ self-references
nodeselector Jul 10, 2026
a342ad9
actions: scaffold lockfile-aware pinning for unpinned-tag
nodeselector Jul 10, 2026
968e775
actions: add lockfile-to-data-extension generator for unpinned-tag
nodeselector Jul 10, 2026
9cc60f5
actions: generate lockfile-pinned data extension during extraction
nodeselector Jul 10, 2026
dea1136
actions: drop machine-specific replace, document lockfile generator l…
nodeselector Jul 10, 2026
40d8b1f
actions: keep lockfile generator go.mod free of local replace
nodeselector Jul 10, 2026
b37ae11
actions: rename self-reference to self repository in unpinned-tag
nodeselector Jul 10, 2026
32f1b67
actions: make lockfile-extension generation atomic
nodeselector Jul 10, 2026
8826d41
actions: drop private actions-lockfile dependency from generator
nodeselector Jul 10, 2026
3016a14
actions: emit empty data list for zero-row lockfile extension
nodeselector Jul 10, 2026
5584b49
actions/unpinned-tag: match lockfile pins case-insensitively on owner…
nodeselector Jul 13, 2026
431736f
actions: use US spelling in lockfile-aware pinning comments
nodeselector Jul 13, 2026
3cb8237
actions: drop lockfile-extension generator from unpinned-tag PR
nodeselector Jul 23, 2026
eff5a35
actions/unpinned-tag: normalize lockfile action identity
nodeselector Aug 31, 2026
c6ea2b1
actions/unpinned-tag: regenerate expected results after sync
nodeselector Aug 31, 2026
75cbb24
Actions: consume extracted lockfile pins
nodeselector Sep 2, 2026
db69b78
Actions tests: add lockfile inline expectations
nodeselector Sep 2, 2026
791af05
Actions tests: share inline expectations utility
nodeselector Sep 2, 2026
d0c1b32
Actions tests: keep inline adapter outside library API
nodeselector Sep 3, 2026
ea26459
Actions lockfiles: ignore schema version value
nodeselector Sep 3, 2026
a6487eb
Actions lockfiles: match repository casing
nodeselector Sep 3, 2026
0106f4e
Actions lockfiles: require repository identity
nodeselector Sep 3, 2026
e733ac6
Revert "Actions lockfiles: require repository identity"
nodeselector 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
category: feature
---
* GitHub Actions databases now extract `actions.lock` files. The new `ActionsLock` class
provides access to their YAML abstract syntax trees.
provides access to their YAML abstract syntax trees and structurally valid workflow pins.
32 changes: 32 additions & 0 deletions actions/ql/lib/codeql/actions/Lock.qll
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,36 @@ private import codeql.actions.ast.internal.Yaml
/** An `actions.lock` file. */
class ActionsLock extends YamlDocument {
ActionsLock() { this.getFile().getBaseName() = "actions.lock" }

/**
* Holds if the lockfile records `nwo` at `ref` for `workflowPath` with a full commit digest.
* Repository pins also cover sub-actions such as `actions/cache/save`.
*/
bindingset[nwo]
predicate pins(string workflowPath, string nwo, string ref) {
this.getFile().getRelativePath() = ".github/workflows/actions.lock" and
exists(
YamlMapping root, YamlSequence workflowPins, YamlScalar pinNode, YamlMapping dependency,
string pin, string pinnedNwo
|
root = this and
root.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
(
nwo.toLowerCase() = pinnedNwo.toLowerCase()
or
nwo.toLowerCase().prefix(pinnedNwo.length() + 1) = pinnedNwo.toLowerCase() + "/"
) and
root.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})$")
)
}
}
13 changes: 13 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,13 @@ where
hasUsesContainerName(uses, name) and
uses.getVersion() = version and
not isTrustedOwner(nwo) and
not isSelfRepository(nwo) and
not exists(UsesStep step |
uses = step and
exists(ActionsLock lock |
lock.pins(step.getLocation().getFile().getRelativePath(), nwo, 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