Skip to content

Commit 5088f9f

Browse files
committed
actions: scaffold lockfile-aware pinning for unpinned-tag
Adds the seam for making actions/unpinned-tag aware of a repository's Actions lockfile (.github/workflows/actions.lock), so that a tag ref bound to a verified commit in the lockfile is not reported as unpinned (Option A from the #755 spike). Introduces the extensible predicate pinnedByLockfileDataModel(workflow_path, nwo, ref) in ConfigExtensions.qll, re-exported through Config.qll, with a data-extension stub in ext/config/pinned_by_lockfile.yml documenting the intended row shape. The query gains a "not pinnedByLockfile(...)" clause keyed on the workflow file's relative path. The predicate is meant to be populated by the CodeQL Actions extractor, which must parse actions.lock at database-creation time using the canonical parser github.com/github/actions-lockfile/go. That extractor work is a separate change and is not implemented here; until it ships the predicate is empty and the new clause is a no-op. A test-scoped data extension exercises the clause end to end. Part of github/actions-dispatch#755.
1 parent dc76ee9 commit 5088f9f

9 files changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* 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.

actions/ql/lib/codeql/actions/config/Config.qll

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ predicate trustedActionsOwnerDataModel(string owner) {
135135
Extensions::trustedActionsOwnerDataModel(owner)
136136
}
137137

138+
/**
139+
* MaD models for `uses` references pinned by the repository's Actions lockfile
140+
* (`.github/workflows/actions.lock`). Populated by the CodeQL Actions extractor; see
141+
* `pinnedByLockfileDataModel` in `ConfigExtensions.qll`.
142+
* Fields:
143+
* - workflow_path: repo-relative path of the file containing the `uses:` reference
144+
* - nwo: owner and name of the referenced action (e.g. `actions/checkout`)
145+
* - ref: the ref as written in `uses:` (e.g. `v4`)
146+
*/
147+
predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref) {
148+
Extensions::pinnedByLockfileDataModel(workflow_path, nwo, ref)
149+
}
150+
138151
/**
139152
* MaD models for untrusted git commands
140153
* Fields:

actions/ql/lib/codeql/actions/config/ConfigExtensions.qll

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ extensible predicate immutableActionsDataModel(string action);
6868
*/
6969
extensible predicate trustedActionsOwnerDataModel(string owner);
7070

71+
/**
72+
* Holds if the `uses` reference `nwo`@`ref` in the workflow or composite action file at
73+
* `workflow_path` is pinned by an entry in the repository's Actions lockfile
74+
* (`.github/workflows/actions.lock`).
75+
*
76+
* This predicate is intended to be populated by the CodeQL Actions extractor, which parses
77+
* `actions.lock` at database-creation time using the canonical lockfile parser at
78+
* `github.com/github/actions-lockfile/go`. Each lockfile entry binds an `nwo`@`ref` to a
79+
* verified commit SHA, which is exactly the pinning evidence the `actions/unpinned-tag` query
80+
* otherwise lacks. Until the extractor populates this predicate it is empty, so any clause that
81+
* consumes it is a clean no-op and behaviour is unchanged for repositories without a lockfile.
82+
*
83+
* Fields:
84+
* - `workflow_path`: repo-relative path of the file containing the `uses:` reference,
85+
* e.g. `.github/workflows/ci.yml`.
86+
* - `nwo`: owner and name of the referenced action, e.g. `actions/checkout`.
87+
* - `ref`: the ref (tag or branch) as written in `uses:`, e.g. `v4`.
88+
*/
89+
extensible predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref);
90+
7191
/**
7292
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
7393
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/actions-all
4+
extensible: pinnedByLockfileDataModel
5+
# `pinnedByLockfileDataModel` records `uses:` references that are pinned by the repository's
6+
# Actions lockfile (`.github/workflows/actions.lock`). It is intended to be populated by the
7+
# CodeQL Actions extractor, which parses the lockfile at database-creation time using the
8+
# canonical Go parser at `github.com/github/actions-lockfile/go`. Each row is
9+
# `[workflow_path, nwo, ref]`:
10+
# - workflow_path: repo-relative path of the file containing the `uses:` reference
11+
# - nwo: owner/name of the referenced action (e.g. `actions/checkout`)
12+
# - ref: the ref as written in `uses:` (e.g. `v4`)
13+
#
14+
# Example of the intended shape (commented out; real data is supplied by the extractor):
15+
# - [".github/workflows/ci.yml", "actions/checkout", "v4"]
16+
#
17+
# Until the extractor populates this predicate it stays empty, so the
18+
# `not pinnedByLockfile(...)` clause in `actions/unpinned-tag` is a no-op and behaviour is
19+
# unchanged for repositories without a lockfile.
20+
data: []

actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+")
3939
bindingset[nwo]
4040
private predicate isSelfReference(string nwo) { nwo.matches("$/%") }
4141

42+
// Holds if `uses` (calling action `nwo` at `version`) is pinned by an entry in the repository's
43+
// Actions lockfile (`.github/workflows/actions.lock`). The underlying `pinnedByLockfileDataModel`
44+
// predicate is populated by the CodeQL Actions extractor when it parses the lockfile at
45+
// database-creation time; until then this is a clean no-op and no lockfile-pinned refs are
46+
// suppressed. See `pinnedByLockfileDataModel` in `ConfigExtensions.qll` for the intended shape.
47+
private predicate pinnedByLockfile(UsesStep uses, string nwo, string version) {
48+
pinnedByLockfileDataModel(uses.getLocation().getFile().getRelativePath(), nwo, version)
49+
}
50+
4251
private predicate getStepContainerName(UsesStep uses, string name) {
4352
exists(Workflow workflow |
4453
uses.getEnclosingWorkflow() = workflow and
@@ -62,6 +71,7 @@ where
6271
uses.getVersion() = version and
6372
not isTrustedOwner(nwo) and
6473
not isSelfReference(nwo) and
74+
not pinnedByLockfile(uses, nwo, version) and
6575
not (if isContainerImage(nwo) then isPinnedContainer(version) else isPinnedCommit(version)) and
6676
not isImmutableAction(uses, nwo)
6777
select uses.getCalleeNode(),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* 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.

actions/ql/test/qlpack.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ dependencies:
99
codeql/immutable-actions-list: ${workspace}
1010
extractor: actions
1111
tests: .
12+
dataExtensions:
13+
- query-tests/Security/CWE-829/*.model.yml
1214
warnOnImplicitThis: true
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
on:
2+
pull_request
3+
4+
jobs:
5+
build:
6+
name: Build and test
7+
runs-on: ubuntu-latest
8+
steps:
9+
# `some-owner/pinned-action@v1` is a tag ref that would normally be reported as an unpinned
10+
# tag. The test data extension `pinned_by_lockfile.model.yml` records it as pinned by the
11+
# repository's Actions lockfile, so the `not pinnedByLockfile(...)` clause suppresses it (this
12+
# fixture is expected to produce no findings). This mirrors what the CodeQL Actions extractor
13+
# will do by parsing `.github/workflows/actions.lock`.
14+
#
15+
# Negative control is provided for free by the many other fixtures in this directory whose tag
16+
# refs are NOT recorded in the data extension and therefore remain reported.
17+
- uses: some-owner/pinned-action@v1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/actions-all
4+
extensible: pinnedByLockfileDataModel
5+
# Test data standing in for what the CodeQL Actions extractor will emit from
6+
# `.github/workflows/actions.lock`. Records `some-owner/pinned-action@v1` in
7+
# `lockfile_pinned.yml` as pinned by the lockfile so the `actions/unpinned-tag`
8+
# query suppresses it.
9+
data:
10+
- [".github/workflows/lockfile_pinned.yml", "some-owner/pinned-action", "v1"]

0 commit comments

Comments
 (0)