From 386872c6683465fbf69c636b5a4e7d3829b8e8de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:16:42 +0000 Subject: [PATCH 1/5] Initial plan From ec12035ac29f9963e27142effb57361c14940ba9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:20:36 +0000 Subject: [PATCH 2/5] Extend unpinned-tag query to scan composite action metadata Agent-Logs-Url: https://github.com/github/codeql/sessions/c52790be-00f6-4250-b46b-38c05365ddd7 Co-authored-by: oscarsj <1410188+oscarsj@users.noreply.github.com> --- .../Security/CWE-829/UnpinnedActionsTag.ql | 25 +++++++++++++------ .../.github/actions/unpinned-tag/action.yml | 6 +++++ .../CWE-829/UnpinnedActionsTag.expected | 1 + 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 actions/ql/test/query-tests/Security/CWE-829/.github/actions/unpinned-tag/action.yml diff --git a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql index c8512e5a1c0c..d70d9a530a0e 100644 --- a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql +++ b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql @@ -31,15 +31,26 @@ private predicate isPinnedContainer(string version) { bindingset[nwo] private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+") } -from UsesStep uses, string nwo, string version, Workflow workflow, string name +private predicate getStepContainerName(UsesStep uses, string name) { + exists(Workflow workflow | + uses.getEnclosingWorkflow() = workflow and + ( + workflow.getName() = name + or + not exists(workflow.getName()) and workflow.getLocation().getFile().getBaseName() = name + ) + ) + or + exists(CompositeAction action | + uses.getEnclosingCompositeAction() = action and + name = action.getLocation().getFile().getBaseName() + ) +} + +from UsesStep uses, string nwo, string version, string name where uses.getCallee() = nwo and - uses.getEnclosingWorkflow() = workflow and - ( - workflow.getName() = name - or - not exists(workflow.getName()) and workflow.getLocation().getFile().getBaseName() = name - ) and + getStepContainerName(uses, name) and uses.getVersion() = version and not isTrustedOwner(nwo) and not (if isContainerImage(nwo) then isPinnedContainer(version) else isPinnedCommit(version)) and diff --git a/actions/ql/test/query-tests/Security/CWE-829/.github/actions/unpinned-tag/action.yml b/actions/ql/test/query-tests/Security/CWE-829/.github/actions/unpinned-tag/action.yml new file mode 100644 index 000000000000..782505cc698d --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-829/.github/actions/unpinned-tag/action.yml @@ -0,0 +1,6 @@ +name: Composite unpinned tag test +runs: + using: "composite" + steps: + - uses: foo/bar@v2 + - uses: foo/bar@25b062c917b0c75f8b47d8469aff6c94ffd89abb diff --git a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected index ed35f1546171..7882cfaeb339 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected @@ -1,3 +1,4 @@ +| .github/actions/unpinned-tag/action.yml:5:13:5:22 | foo/bar@v2 | Unpinned 3rd party Action 'action.yml' step $@ uses 'foo/bar' with ref 'v2', not a pinned commit hash | .github/actions/unpinned-tag/action.yml:5:7:6:60 | Uses Step | Uses Step | | .github/workflows/actor_trusted_checkout.yml:19:13:19:36 | completely/fakeaction@v2 | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'completely/fakeaction' with ref 'v2', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:19:7:23:4 | Uses Step | Uses Step | | .github/workflows/actor_trusted_checkout.yml:23:13:23:37 | fakerepo/comment-on-pr@v1 | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'fakerepo/comment-on-pr' with ref 'v1', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:23:7:26:21 | Uses Step | Uses Step | | .github/workflows/artifactpoisoning21.yml:13:15:13:49 | dawidd6/action-download-artifact@v2 | Unpinned 3rd party Action 'Pull Request Open' step $@ uses 'dawidd6/action-download-artifact' with ref 'v2', not a pinned commit hash | .github/workflows/artifactpoisoning21.yml:13:9:18:6 | Uses Step | Uses Step | From e598c56c64e24638733b08c9e14c5deda446579f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20San=20Jos=C3=A9?= Date: Mon, 20 Apr 2026 12:00:37 +0200 Subject: [PATCH 3/5] update and fix tests --- .../query-tests/Security/CWE-829/UnpinnedActionsTag.expected | 2 +- .../Security/CWE-829/UntrustedCheckoutCritical.expected | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected index 7882cfaeb339..29568087d0ed 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected @@ -1,4 +1,4 @@ -| .github/actions/unpinned-tag/action.yml:5:13:5:22 | foo/bar@v2 | Unpinned 3rd party Action 'action.yml' step $@ uses 'foo/bar' with ref 'v2', not a pinned commit hash | .github/actions/unpinned-tag/action.yml:5:7:6:60 | Uses Step | Uses Step | +| .github/actions/unpinned-tag/action.yml:5:13:5:22 | foo/bar@v2 | Unpinned 3rd party Action 'action.yml' step $@ uses 'foo/bar' with ref 'v2', not a pinned commit hash | .github/actions/unpinned-tag/action.yml:5:7:6:4 | Uses Step | Uses Step | | .github/workflows/actor_trusted_checkout.yml:19:13:19:36 | completely/fakeaction@v2 | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'completely/fakeaction' with ref 'v2', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:19:7:23:4 | Uses Step | Uses Step | | .github/workflows/actor_trusted_checkout.yml:23:13:23:37 | fakerepo/comment-on-pr@v1 | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'fakerepo/comment-on-pr' with ref 'v1', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:23:7:26:21 | Uses Step | Uses Step | | .github/workflows/artifactpoisoning21.yml:13:15:13:49 | dawidd6/action-download-artifact@v2 | Unpinned 3rd party Action 'Pull Request Open' step $@ uses 'dawidd6/action-download-artifact' with ref 'v2', not a pinned commit hash | .github/workflows/artifactpoisoning21.yml:13:9:18:6 | Uses Step | Uses Step | diff --git a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected index 39e54b2bbaed..7b63fb560cbe 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected @@ -8,6 +8,7 @@ edges | .github/actions/download-artifact/action.yaml:25:7:29:4 | Run Step | .github/actions/download-artifact/action.yaml:29:7:32:18 | Run Step | | .github/actions/download-artifact/action.yaml:29:7:32:18 | Run Step | .github/workflows/artifactpoisoning91.yml:19:9:25:6 | Run Step: metadata | | .github/actions/download-artifact/action.yaml:29:7:32:18 | Run Step | .github/workflows/resolve-args.yml:22:9:36:13 | Run Step: resolve-step | +| .github/actions/unpinned-tag/action.yml:5:7:6:4 | Uses Step | .github/actions/unpinned-tag/action.yml:6:7:6:61 | Uses Step | | .github/workflows/actor_trusted_checkout.yml:9:7:14:4 | Uses Step | .github/workflows/actor_trusted_checkout.yml:14:7:15:4 | Uses Step | | .github/workflows/actor_trusted_checkout.yml:14:7:15:4 | Uses Step | .github/workflows/actor_trusted_checkout.yml:15:7:19:4 | Run Step | | .github/workflows/actor_trusted_checkout.yml:15:7:19:4 | Run Step | .github/workflows/actor_trusted_checkout.yml:19:7:23:4 | Uses Step | From ca68274ec382300d8912bd8bbabc45d6a7690b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20San=20Jos=C3=A9?= Date: Mon, 20 Apr 2026 12:43:25 +0200 Subject: [PATCH 4/5] Add changelog --- .../change-notes/2026-04-20-unpinned-tag-composite-actions.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 actions/ql/src/change-notes/2026-04-20-unpinned-tag-composite-actions.md diff --git a/actions/ql/src/change-notes/2026-04-20-unpinned-tag-composite-actions.md b/actions/ql/src/change-notes/2026-04-20-unpinned-tag-composite-actions.md new file mode 100644 index 000000000000..7e00965a17cf --- /dev/null +++ b/actions/ql/src/change-notes/2026-04-20-unpinned-tag-composite-actions.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `actions/unpinned-tag` query now analyzes composite action metadata (`action.yml`/`action.yaml` files) in addition to workflow files, providing more comprehensive detection of unpinned action references across the entire Actions ecosystem. \ No newline at end of file From b2046034f112cdfb01f1389218c56be3cdb510e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:01:57 +0000 Subject: [PATCH 5/5] Update UnpinnedActionsTag query metadata scope Agent-Logs-Url: https://github.com/github/codeql/sessions/5425ff86-b998-4c7b-9447-52c8ae74a7a2 Co-authored-by: oscarsj <1410188+oscarsj@users.noreply.github.com> --- actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql index d70d9a530a0e..7b5e5a117cbe 100644 --- a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql +++ b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql @@ -1,5 +1,5 @@ /** - * @name Unpinned tag for a non-immutable Action in workflow + * @name Unpinned tag for a non-immutable Action in workflow or composite action * @description Using a tag for a non-immutable Action that is not pinned to a commit can lead to executing an untrusted Action through a supply chain attack. * @kind problem * @security-severity 5.0