Skip to content

Commit 8ed7d3d

Browse files
committed
Add Minimum Permissions Map
* detect commonly used GitHub owned actions use a map to suggest the minimum permissions needed for the GITHUB_TOKEN
1 parent 90faab4 commit 8ed7d3d

File tree

5 files changed

+90
-11
lines changed

5 files changed

+90
-11
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ predicate vulnerableActionsDataModel(
126126
*/
127127
predicate immutableActionsDataModel(string action) { Extensions::immutableActionsDataModel(action) }
128128

129+
/**
130+
* MaD models for minimum permissions for actions
131+
* Fields:
132+
* - action: action name
133+
* - minimum_permissions: list of minimum permissions
134+
*/
135+
predicate minimumPermissionsDataModel(
136+
string action, string minimum_permissions
137+
) {
138+
Extensions::minimumPermissionsDataModel(action, minimum_permissions)
139+
}
140+
129141
/**
130142
* MaD models for untrusted git commands
131143
* Fields:

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

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ extensible predicate vulnerableActionsDataModel(
6363
*/
6464
extensible predicate immutableActionsDataModel(string action);
6565

66+
/**
67+
* Holds for actions that have a minimum permissions definition.
68+
*/
69+
extensible predicate minimumPermissionsDataModel(string action, string minimum_permissions);
70+
6671
/**
6772
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
6873
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import actions
2+
class MinimumActionsPermissions extends UsesStep {
3+
string action;
4+
string minimum_permissions;
5+
6+
MinimumActionsPermissions() {
7+
minimumPermissionsDataModel(action, minimum_permissions) and
8+
this.getCallee() = action
9+
}
10+
11+
string getMinimumPermissions() { result = minimum_permissions }
12+
13+
string getAction() { result = action }
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
:
2+
- addsTo:
3+
pack: github/actions-all
4+
extensible: minimumPermissionsDataModel
5+
data:
6+
- ["actions/cache", "{}"]
7+
- ["actions/setup-node", "contents:read"]
8+
- ["actions/upload-artifact", "{}"]
9+
- ["actions/setup-python", "contents:read"]
10+
- ["actions/download-artifact", "{}"]
11+
- ["actions/github-script", "It depends on what the script does"]
12+
- ["actions/setup-java", "contents:read"]
13+
- ["actions/setup-go", "contents:read"]
14+
- ["actions/setup-dotnet", "contents:read"]
15+
- ["actions/labeler", "contents:read, pull-requests:write"]
16+
- ["actions/attest", "id-token:write, attestations:write"]
17+
- ["actions/add-to-project", "repository-projects:read, repository-projects:write, issues:read, pull-requests:read"]
18+
- ["actions/dependency-review-action", "contents:read"]
19+
- ["actions/attest-sbom", "id-token:write, attestations:write"]
20+
- ["actions/stale", "contents:write, issues:write, pull-requests:write"]
21+
- ["actions/attest-build-provenance", "id-token:write, attestations:write"]
22+
- ["actions/jekyll-build-pages", "contents:read, pages:write, id-token:write"]
23+
- ["actions/publish-action", "contents:write"]
24+
- ["actions/version-package-tools", "contents:read, actions:read"]
25+
- ["actions/reusable-workflows", "contents:read, actions:read"]

actions/ql/src/Security/CWE-275/MissingActionsPermissions.ql

+34-11
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,38 @@
1111
* external/cwe/cwe-275
1212
*/
1313

14-
import actions
14+
import actions
15+
import codeql.actions.security.MinimumActionsPermissions
1516

16-
from Job job
17-
where
18-
not exists(job.getPermissions()) and
19-
not exists(job.getEnclosingWorkflow().getPermissions()) and
20-
// exists a trigger event that is not a workflow_call
21-
exists(Event e |
22-
e = job.getATriggerEvent() and
23-
not e.getName() = "workflow_call"
24-
)
25-
select job, "Actions Job or Workflow does not set permissions"
17+
// Returns the minimum permissions for all of the uses steps
18+
// that are children of the job separated by a comma
19+
// e.g. "contents: read, packages: write". If we cannot determine
20+
// the permission we fallback to "unknown"
21+
string getMinPermissions(Job job) {
22+
if unknownPermissions(job) = true then result = "unknown" else
23+
result = minPermissions(job)
24+
}
25+
26+
string minPermissions(Job job) {
27+
result = concat(job.getAChildNode*().(MinimumActionsPermissions).getMinimumPermissions(), ", ")
28+
}
29+
30+
// Holds if we cannot determine the permissions for the uses step
31+
// using the data extension or there are no uses steps
32+
// that are children of the job
33+
boolean unknownPermissions(Job job) {
34+
minPermissions(job) = "" and result = true or count(job.getAChildNode*().(MinimumActionsPermissions)) = 0 and result = true
35+
}
36+
37+
from Job job
38+
where
39+
not exists(job.getPermissions()) and
40+
not exists(job.getEnclosingWorkflow().getPermissions()) and
41+
// exists a trigger event that is not a workflow_call
42+
exists(Event e |
43+
e = job.getATriggerEvent() and
44+
not e.getName() = "workflow_call"
45+
)
46+
select job,
47+
"Actions Job or Workflow does not set permissions. Recommended minimum permissions are ($@)",
48+
job, getMinPermissions(job)

0 commit comments

Comments
 (0)