ruby: add MaD model for permissions needed by actions

Use this to suggest minimal set of nedded permissions
This commit is contained in:
yoff
2025-03-28 12:57:58 +01:00
parent 279e9e2d70
commit e7bb47f335
8 changed files with 99 additions and 6 deletions

View File

@@ -242,6 +242,8 @@ class Step extends AstNode instanceof StepImpl {
If getIf() { result = super.getIf() }
AstNode getUses() { result = super.getUses() }
StepsContainer getContainer() { result = super.getContainer() }
Step getNextStep() { result = super.getNextStep() }

View File

@@ -1194,6 +1194,8 @@ class StepImpl extends AstNodeImpl, TStepNode {
/** Gets the value of the `if` field in this step, if any. */
IfImpl getIf() { result.getNode() = n.lookup("if") }
AstNodeImpl getUses() { result.getNode() = n.lookup("uses") }
/** Gets the Runs or LocalJob that this step is in. */
StepsContainerImpl getContainer() {
result = this.getParentNode().(RunsImpl) or

View File

@@ -154,3 +154,13 @@ predicate untrustedGitCommandDataModel(string cmd_regex, string flag) {
predicate untrustedGhCommandDataModel(string cmd_regex, string flag) {
Extensions::untrustedGhCommandDataModel(cmd_regex, flag)
}
/**
* MaD models for permissions needed by actions
* Fields:
* - action: action name
* - permission: permission name
*/
predicate actionsPermissionsDataModel(string action, string permission) {
Extensions::actionsPermissionsDataModel(action, permission)
}

View File

@@ -77,3 +77,8 @@ extensible predicate untrustedGitCommandDataModel(string cmd_regex, string flag)
* Holds for gh commands that may introduce untrusted data
*/
extensible predicate untrustedGhCommandDataModel(string cmd_regex, string flag);
/**
* Holds if `action` needs `permission` to run.
*/
extensible predicate actionsPermissionsDataModel(string action, string permission);

View File

@@ -0,0 +1,37 @@
extensions:
- addsTo:
pack: codeql/actions-all
extensible: actionsPermissionsDataModel
data:
- ["actions/checkout", "contents: read"]
- ["actions/setup-node", "contents: read"]
- ["actions/setup-python", "contents: read"]
- ["actions/setup-java", "contents: read"]
- ["actions/setup-go", "contents: read"]
- ["actions/setup-dotnet", "contents: read"]
- ["actions/labeler", "contents: read"]
- ["actions/labeler", "pull-requests: write"]
- ["actions/attest", "id-token: write"]
- ["actions/attest", "attestations: write"]
# No permissions needed for actions/add-to-project
- ["actions/dependency-review-action", "contents: read"]
- ["actions/attest-sbom", "id-token: write"]
- ["actions/attest-sbom", "attestations: write"]
- ["actions/stale", "contents: write"]
- ["actions/stale", "issues: write"]
- ["actions/stale", "pull-requests: write"]
- ["actions/attest-build-provenance", "id-token: write"]
- ["actions/attest-build-provenance", "attestations: write"]
- ["actions/jekyll-build-pages", "contents: read"]
- ["actions/jekyll-build-pages", "pages: write"]
- ["actions/jekyll-build-pages", "id-token: write"]
- ["actions/publish-action", "contents: write"]
- ["actions/versions-package-tools", "contents: read"]
- ["actions/versions-package-tools", "actions: read"]
- ["actions/reusable-workflows", "contents: read"]
- ["actions/reusable-workflows", "actions: read"]
# TODO: Add permissions for actions/download-artifact
# TODO: Add permissions for actions/upload-artifact
# TODO: Add permissions for actions/cache

View File

@@ -14,7 +14,28 @@
import actions
from Job job
Step stepInJob(Job job) { result = job.(LocalJob).getAStep() }
bindingset[fullActionSelector]
string versionedAction(string fullActionSelector) {
result = fullActionSelector.substring(0, fullActionSelector.indexOf("@"))
or
not exists(fullActionSelector.indexOf("@")) and
result = fullActionSelector
}
string stepUses(Step step) { result = step.getUses().(ScalarValue).getValue() }
string jobNeedsPersmission(Job job) {
actionsPermissionsDataModel(versionedAction(stepUses(stepInJob(job))), result)
}
string permissionsForJob(Job job) {
result =
"{" + concat(string permission | permission = jobNeedsPersmission(job) | permission, ", ") + "}"
}
from Job job, string permissions
where
not exists(job.getPermissions()) and
not exists(job.getEnclosingWorkflow().getPermissions()) and
@@ -22,5 +43,7 @@ where
exists(Event e |
e = job.getATriggerEvent() and
not e.getName() = "workflow_call"
)
select job, "Actions Job or Workflow does not set permissions"
) and
permissions = permissionsForJob(job)
select job,
"Actions Job or Workflow does not set permissions. A minimal set might be " + permissions

View File

@@ -0,0 +1,13 @@
on:
workflow_call:
workflow_dispatch:
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/jekyll-build-pages

View File

@@ -1,3 +1,4 @@
| .github/workflows/perms1.yml:6:5:9:32 | Job: build | Actions Job or Workflow does not set permissions |
| .github/workflows/perms2.yml:6:5:10:2 | Job: build | Actions Job or Workflow does not set permissions |
| .github/workflows/perms5.yml:7:5:10:32 | Job: build | Actions Job or Workflow does not set permissions |
| .github/workflows/perms1.yml:6:5:9:32 | Job: build | Actions Job or Workflow does not set permissions. A minimal set might be {contents: read} |
| .github/workflows/perms2.yml:6:5:10:2 | Job: build | Actions Job or Workflow does not set permissions. A minimal set might be {contents: read} |
| .github/workflows/perms5.yml:7:5:10:32 | Job: build | Actions Job or Workflow does not set permissions. A minimal set might be {contents: read} |
| .github/workflows/perms6.yml:7:5:11:39 | Job: build | Actions Job or Workflow does not set permissions. A minimal set might be {contents: read, id-token: write, pages: write} |