From bdf87109cc5f586c00645f9c1ff606baa9411e75 Mon Sep 17 00:00:00 2001 From: Paolo Tranquilli Date: Mon, 24 Aug 2026 16:05:48 +0200 Subject: [PATCH 1/3] Support distrusting Actions owners via `!owner` entries Lets `trustedActionsOwnerDataModel` remove an owner (including the first-party `actions`, `github` and `advanced-security`) with a `!`-prefixed entry, so unpinned tags for those Actions are reported by `actions/unpinned-tag`. Unambiguous because owner names can never start with `!`. Closes #22409 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../2026-08-24-untrusted-actions-owner.md | 4 ++++ .../ql/lib/codeql/actions/config/Config.qll | 9 ++++++++- .../workflows/unpinned_first_party.yml | 13 +++++++++++++ .../UnpinnedActionsTag.expected | 2 ++ .../UnpinnedActionsTag.ext.yml | 8 ++++++++ .../UnpinnedActionsTag.qlref | 1 + ...customizing-library-models-for-actions.rst | 19 +++++++++++++++++++ 7 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 actions/ql/lib/change-notes/2026-08-24-untrusted-actions-owner.md create mode 100644 actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected create mode 100644 actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.ext.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref diff --git a/actions/ql/lib/change-notes/2026-08-24-untrusted-actions-owner.md b/actions/ql/lib/change-notes/2026-08-24-untrusted-actions-owner.md new file mode 100644 index 000000000000..b244b67bef36 --- /dev/null +++ b/actions/ql/lib/change-notes/2026-08-24-untrusted-actions-owner.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `trustedActionsOwnerDataModel` extensible predicate, used by the `actions/unpinned-tag` query, now supports removing an owner from the trusted set by adding an entry prefixed with `!` (for example, `!github`). This makes it possible to distrust first-party owners (`actions`, `github`, `advanced-security`) so that unpinned tags for their Actions are reported. diff --git a/actions/ql/lib/codeql/actions/config/Config.qll b/actions/ql/lib/codeql/actions/config/Config.qll index 27e24514c091..f6b8d0c979f3 100644 --- a/actions/ql/lib/codeql/actions/config/Config.qll +++ b/actions/ql/lib/codeql/actions/config/Config.qll @@ -130,9 +130,16 @@ predicate immutableActionsDataModel(string action) { Extensions::immutableAction * MaD models for trusted actions owners * Fields: * - owner: owner name + * + * An entry may be prefixed with `!` to remove an owner from the trusted set, + * overriding any plain entry for the same owner (for example, `!github` distrusts + * the first-party `github` owner). This is unambiguous because GitHub owner names + * can never start with `!`. */ predicate trustedActionsOwnerDataModel(string owner) { - Extensions::trustedActionsOwnerDataModel(owner) + Extensions::trustedActionsOwnerDataModel(owner) and + not owner.matches("!%") and + not Extensions::trustedActionsOwnerDataModel("!" + owner) } /** diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml new file mode 100644 index 000000000000..3161809fd0bf --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml @@ -0,0 +1,13 @@ +name: Unpinned first-party actions +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + # `actions` is distrusted via `!actions`, and this action is not immutable, + # so this unpinned tag is reported. + - uses: actions/first-interaction@v1 + # `github` remains trusted, so this unpinned tag is not reported. + - uses: github/issue-labeler@v3.0 + # Third-party owner is always reported. + - uses: foo/bar@v1 diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected new file mode 100644 index 000000000000..9b358e29773d --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected @@ -0,0 +1,2 @@ +| .github/workflows/unpinned_first_party.yml:9:15:9:42 | actions/first-interaction@v1 | Unpinned 3rd party Action 'Unpinned first-party actions' step $@ uses 'actions/first-interaction' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_first_party.yml:9:9:11:6 | Uses Step | Uses Step | +| .github/workflows/unpinned_first_party.yml:13:15:13:24 | foo/bar@v1 | Unpinned 3rd party Action 'Unpinned first-party actions' step $@ uses 'foo/bar' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_first_party.yml:13:9:13:25 | Uses Step | Uses Step | diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.ext.yml b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.ext.yml new file mode 100644 index 000000000000..45aea89e7186 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.ext.yml @@ -0,0 +1,8 @@ +extensions: + - addsTo: + pack: codeql/actions-all + extensible: trustedActionsOwnerDataModel + data: + # Distrust the first-party `actions` owner so unpinned tags for its + # Actions are reported. `github` is intentionally left trusted. + - ["!actions"] diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref new file mode 100644 index 000000000000..8c9db66bf6bb --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref @@ -0,0 +1 @@ +Security/CWE-829/UnpinnedActionsTag.ql diff --git a/docs/codeql/codeql-language-guides/customizing-library-models-for-actions.rst b/docs/codeql/codeql-language-guides/customizing-library-models-for-actions.rst index 0b78b37359f4..3f37d728dbf2 100644 --- a/docs/codeql/codeql-language-guides/customizing-library-models-for-actions.rst +++ b/docs/codeql/codeql-language-guides/customizing-library-models-for-actions.rst @@ -83,3 +83,22 @@ To allow any Action from the publisher ``octodemo``, such as ``octodemo/3rd-part 3. Ensure that the model pack is included in your CodeQL analysis. By following these steps, you will add ``octodemo`` to the list of trusted Action publishers, and the query will no longer generate security alerts for unpinned tags from this publisher. For more information, see `Extending CodeQL coverage with CodeQL model packs in default setup `_ and `Creating and working with CodeQL packs `_. + +Example: Remove a trusted Actions publisher for the ``actions/unpinned-tag`` query +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +GitHub's own organizations (``actions``, ``github`` and ``advanced-security``) are trusted by default. If you want unpinned tags for these first-party Actions to be reported as well, you can remove an owner from the trusted list by adding an entry prefixed with ``!``. A ``!`` entry always takes precedence over a plain entry for the same owner. + +To distrust the first-party ``github`` owner, add a data extension file with the following content: + +.. code-block:: yaml + + extensions: + - addsTo: + pack: codeql/actions-all + extensible: trustedActionsOwnerDataModel + data: + - ["!github"] + +With this in place, the query will once again report unpinned tags for Actions published by ``github``. + From fe3eaa8c72796a14dcacac1739e939ef50ec1886 Mon Sep 17 00:00:00 2001 From: Paolo Tranquilli Date: Mon, 24 Aug 2026 18:57:52 +0200 Subject: [PATCH 2/3] Use inline expectations for Actions test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../test/InlineExpectationsTestQuery.ql | 23 +++++++++++++++++++ .../internal/InlineExpectationsTestImpl.qll | 13 +++++++++++ .../workflows/unpinned_first_party.yml | 4 ++-- .../UnpinnedActionsTag.expected | 2 +- .../UnpinnedActionsTag.qlref | 3 ++- 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql create mode 100644 actions/ql/lib/codeql/actions/test/internal/InlineExpectationsTestImpl.qll diff --git a/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql b/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql new file mode 100644 index 000000000000..43fcd031637a --- /dev/null +++ b/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql @@ -0,0 +1,23 @@ +/** + * @kind test-postprocess + */ + +private import actions +private import codeql.Locations +private import codeql.files.FileSystem +private import codeql.util.test.InlineExpectationsTest as T +private import codeql.actions.test.internal.InlineExpectationsTestImpl +import T::TestPostProcessing +import T::TestPostProcessing::Make + +private module Input implements T::TestPostProcessing::InputSig { + string getRelativeUrl(Location location) { + exists(File f, int startline, int startcolumn, int endline, int endcolumn | + location.hasLocationInfo(_, startline, startcolumn, endline, endcolumn) and + f = location.getFile() + | + result = + f.getRelativePath() + ":" + startline + ":" + startcolumn + ":" + endline + ":" + endcolumn + ) + } +} diff --git a/actions/ql/lib/codeql/actions/test/internal/InlineExpectationsTestImpl.qll b/actions/ql/lib/codeql/actions/test/internal/InlineExpectationsTestImpl.qll new file mode 100644 index 000000000000..5498ff795eb5 --- /dev/null +++ b/actions/ql/lib/codeql/actions/test/internal/InlineExpectationsTestImpl.qll @@ -0,0 +1,13 @@ +private import codeql.Locations as L +private import codeql.actions.ast.internal.Yaml +private import codeql.util.test.InlineExpectationsTest + +module Impl implements InlineExpectationsTestSig { + class ExpectationComment extends YamlNode { + ExpectationComment() { this.toString().matches("%$ %") } + + string getContents() { result = "$ " + this.toString().regexpCapture(".*\\$ (.*)", 1) } + } + + class Location = L::Location; +} diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml index 3161809fd0bf..6617f509693d 100644 --- a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/.github/workflows/unpinned_first_party.yml @@ -6,8 +6,8 @@ jobs: steps: # `actions` is distrusted via `!actions`, and this action is not immutable, # so this unpinned tag is reported. - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v1 # $ Alert # `github` remains trusted, so this unpinned tag is not reported. - uses: github/issue-labeler@v3.0 # Third-party owner is always reported. - - uses: foo/bar@v1 + - uses: foo/bar@v1 # $ Alert diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected index 9b358e29773d..a047196d85fa 100644 --- a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.expected @@ -1,2 +1,2 @@ | .github/workflows/unpinned_first_party.yml:9:15:9:42 | actions/first-interaction@v1 | Unpinned 3rd party Action 'Unpinned first-party actions' step $@ uses 'actions/first-interaction' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_first_party.yml:9:9:11:6 | Uses Step | Uses Step | -| .github/workflows/unpinned_first_party.yml:13:15:13:24 | foo/bar@v1 | Unpinned 3rd party Action 'Unpinned first-party actions' step $@ uses 'foo/bar' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_first_party.yml:13:9:13:25 | Uses Step | Uses Step | +| .github/workflows/unpinned_first_party.yml:13:15:13:24 | foo/bar@v1 | Unpinned 3rd party Action 'Unpinned first-party actions' step $@ uses 'foo/bar' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_first_party.yml:13:9:13:35 | Uses Step | Uses Step | diff --git a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref index 8c9db66bf6bb..9d46bb88b264 100644 --- a/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref +++ b/actions/ql/test/query-tests/Security/CWE-829-untrusted-owner/UnpinnedActionsTag.qlref @@ -1 +1,2 @@ -Security/CWE-829/UnpinnedActionsTag.ql +query: Security/CWE-829/UnpinnedActionsTag.ql +postprocess: codeql/actions/test/InlineExpectationsTestQuery.ql From 13a73b5cca15ff67cad8ac731c9053e085cf6780 Mon Sep 17 00:00:00 2001 From: Paolo Tranquilli Date: Mon, 24 Aug 2026 19:08:32 +0200 Subject: [PATCH 3/3] Drop redundant `codeql.files.FileSystem` import `codeql.Locations` already re-exports it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql | 1 - 1 file changed, 1 deletion(-) diff --git a/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql b/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql index 43fcd031637a..4fb223e63092 100644 --- a/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql +++ b/actions/ql/lib/codeql/actions/test/InlineExpectationsTestQuery.ql @@ -4,7 +4,6 @@ private import actions private import codeql.Locations -private import codeql.files.FileSystem private import codeql.util.test.InlineExpectationsTest as T private import codeql.actions.test.internal.InlineExpectationsTestImpl import T::TestPostProcessing