Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added a new extensible predicate `pinnedByLockfileDataModel(workflow_path, nwo, ref)`, which records `uses:` references that are pinned by a repository's Actions lockfile (`.github/workflows/actions.lock`). It is intended to be populated by the CodeQL Actions extractor and is currently unpopulated, so it has no effect until that support ships.
13 changes: 13 additions & 0 deletions actions/ql/lib/codeql/actions/config/Config.qll
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ predicate trustedActionsOwnerDataModel(string owner) {
Extensions::trustedActionsOwnerDataModel(owner)
}

/**
* MaD models for `uses` references pinned by the repository's Actions lockfile
* (`.github/workflows/actions.lock`). Populated by the CodeQL Actions extractor; see
* `pinnedByLockfileDataModel` in `ConfigExtensions.qll`.
* Fields:
* - workflow_path: repo-relative path of the file containing the `uses:` reference
* - nwo: owner and name of the referenced action (e.g. `actions/checkout`)
* - ref: the ref as written in `uses:` (e.g. `v4`)
*/
predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref) {
Extensions::pinnedByLockfileDataModel(workflow_path, nwo, ref)
}

/**
* MaD models for untrusted git commands
* Fields:
Expand Down
20 changes: 20 additions & 0 deletions actions/ql/lib/codeql/actions/config/ConfigExtensions.qll
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@
*/
extensible predicate trustedActionsOwnerDataModel(string owner);

/**
* Holds if the `uses` reference `nwo`@`ref` in the workflow or composite action file at
* `workflow_path` is pinned by an entry in the repository's Actions lockfile
* (`.github/workflows/actions.lock`).
*
* This predicate is intended to be populated by the CodeQL Actions extractor, which parses
* `actions.lock` at database-creation time using the canonical lockfile parser at
* `github.com/github/actions-lockfile/go`. Each lockfile entry binds an `nwo`@`ref` to a
* verified commit SHA, which is exactly the pinning evidence the `actions/unpinned-tag` query
* otherwise lacks. Until the extractor populates this predicate it is empty, so any clause that
* consumes it is a clean no-op and behaviour is unchanged for repositories without a lockfile.
*
* Fields:
* - `workflow_path`: repo-relative path of the file containing the `uses:` reference,
* e.g. `.github/workflows/ci.yml`.
* - `nwo`: owner and name of the referenced action, e.g. `actions/checkout`.
* - `ref`: the ref (tag or branch) as written in `uses:`, e.g. `v4`.
*/

Check warning

Code scanning / CodeQL

Misspelling Warning

This comment contains the non-US spelling 'behaviour', which should instead be 'behavior'.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
extensible predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref);

/**
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
*/
Expand Down
20 changes: 20 additions & 0 deletions actions/ql/lib/ext/config/pinned_by_lockfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
extensions:
- addsTo:
pack: codeql/actions-all
extensible: pinnedByLockfileDataModel
# `pinnedByLockfileDataModel` records `uses:` references that are pinned by the repository's
# Actions lockfile (`.github/workflows/actions.lock`). It is intended to be populated by the
# CodeQL Actions extractor, which parses the lockfile at database-creation time using the
# canonical Go parser at `github.com/github/actions-lockfile/go`. Each row is
# `[workflow_path, nwo, ref]`:
# - workflow_path: repo-relative path of the file containing the `uses:` reference
# - nwo: owner/name of the referenced action (e.g. `actions/checkout`)
# - ref: the ref as written in `uses:` (e.g. `v4`)
#
# Example of the intended shape (commented out; real data is supplied by the extractor):
# - [".github/workflows/ci.yml", "actions/checkout", "v4"]
#
# Until the extractor populates this predicate it stays empty, so the
# `not pinnedByLockfile(...)` clause in `actions/unpinned-tag` is a no-op and behaviour is
# unchanged for repositories without a lockfile.
data: []
17 changes: 17 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,21 @@ private predicate isPinnedContainer(string version) {
bindingset[nwo]
private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+") }

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

// Holds if `uses` (calling action `nwo` at `version`) is pinned by an entry in the repository's
// Actions lockfile (`.github/workflows/actions.lock`). The underlying `pinnedByLockfileDataModel`
// predicate is populated by the CodeQL Actions extractor when it parses the lockfile at
// database-creation time; until then this is a clean no-op and no lockfile-pinned refs are
// suppressed. See `pinnedByLockfileDataModel` in `ConfigExtensions.qll` for the intended shape.
private predicate pinnedByLockfile(UsesStep uses, string nwo, string version) {
pinnedByLockfileDataModel(uses.getLocation().getFile().getRelativePath(), nwo, version)
}

private predicate getStepContainerName(UsesStep uses, string name) {
exists(Workflow workflow |
uses.getEnclosingWorkflow() = workflow and
Expand All @@ -55,6 +70,8 @@ where
getStepContainerName(uses, name) and
uses.getVersion() = version and
not isTrustedOwner(nwo) and
not isSelfReference(nwo) and
not pinnedByLockfile(uses, nwo, version) and
not (if isContainerImage(nwo) then isPinnedContainer(version) else isPinnedCommit(version)) and
not isImmutableAction(uses, nwo)
select uses.getCalleeNode(),
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 `uses:` references that are recorded as pinned by a repository's Actions lockfile (`.github/workflows/actions.lock`) via the new `pinnedByLockfileDataModel` extensible predicate. This predicate is populated by the CodeQL Actions extractor and has no effect until that support ships.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `actions/unpinned-tag` query no longer reports `$/` same-repo self-references (e.g. `uses: $/path/to/action`), which are inherently pinned to the running commit, just like `./` local references.
2 changes: 2 additions & 0 deletions actions/ql/test/qlpack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ dependencies:
codeql/immutable-actions-list: ${workspace}
extractor: actions
tests: .
dataExtensions:
- query-tests/Security/CWE-829/*.model.yml
warnOnImplicitThis: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
on:
pull_request

jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
# `some-owner/pinned-action@v1` is a tag ref that would normally be reported as an unpinned
# tag. The test data extension `pinned_by_lockfile.model.yml` records it as pinned by the
# repository's Actions lockfile, so the `not pinnedByLockfile(...)` clause suppresses it (this
# fixture is expected to produce no findings). This mirrors what the CodeQL Actions extractor
# will do by parsing `.github/workflows/actions.lock`.
#
# Negative control is provided for free by the many other fixtures in this directory whose tag
# refs are NOT recorded in the data extension and therefore remain reported.
- uses: some-owner/pinned-action@v1
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-repo self-reference resolved at the running commit. It is inherently
# pinned (like `./` local 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 isSelfReference(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
@@ -0,0 +1,10 @@
extensions:
- addsTo:
pack: codeql/actions-all
extensible: pinnedByLockfileDataModel
# Test data standing in for what the CodeQL Actions extractor will emit from
# `.github/workflows/actions.lock`. Records `some-owner/pinned-action@v1` in
# `lockfile_pinned.yml` as pinned by the lockfile so the `actions/unpinned-tag`
# query suppresses it.
data:
- [".github/workflows/lockfile_pinned.yml", "some-owner/pinned-action", "v1"]
Loading