diff --git a/actions/ql/lib/codeql/actions/config/Config.qll b/actions/ql/lib/codeql/actions/config/Config.qll index 265d4bd820f8..ca16e9d3b6d5 100644 --- a/actions/ql/lib/codeql/actions/config/Config.qll +++ b/actions/ql/lib/codeql/actions/config/Config.qll @@ -126,6 +126,18 @@ predicate vulnerableActionsDataModel( */ predicate immutableActionsDataModel(string action) { Extensions::immutableActionsDataModel(action) } +/** + * MaD models for minimum permissions for actions + * Fields: + * - action: action name + * - minimum_permissions: list of minimum permissions + */ +predicate minimumPermissionsDataModel( + string action, string minimum_permissions +) { + Extensions::minimumPermissionsDataModel(action, minimum_permissions) +} + /** * MaD models for untrusted git commands * Fields: diff --git a/actions/ql/lib/codeql/actions/config/ConfigExtensions.qll b/actions/ql/lib/codeql/actions/config/ConfigExtensions.qll index 99ad7eb8df1b..7f909adf7f93 100644 --- a/actions/ql/lib/codeql/actions/config/ConfigExtensions.qll +++ b/actions/ql/lib/codeql/actions/config/ConfigExtensions.qll @@ -63,6 +63,11 @@ extensible predicate vulnerableActionsDataModel( */ extensible predicate immutableActionsDataModel(string action); +/** + * Holds for actions that have a minimum permissions definition. + */ +extensible predicate minimumPermissionsDataModel(string action, string minimum_permissions); + /** * Holds for git commands that may introduce untrusted data when called on an attacker controlled branch. */ diff --git a/actions/ql/lib/codeql/actions/security/MinimumActionsPermissions.qll b/actions/ql/lib/codeql/actions/security/MinimumActionsPermissions.qll new file mode 100644 index 000000000000..a11371a93d8c --- /dev/null +++ b/actions/ql/lib/codeql/actions/security/MinimumActionsPermissions.qll @@ -0,0 +1,14 @@ +import actions +class MinimumActionsPermissions extends UsesStep { + string action; + string minimum_permissions; + + MinimumActionsPermissions() { + minimumPermissionsDataModel(action, minimum_permissions) and + this.getCallee() = action + } + + string getMinimumPermissions() { result = minimum_permissions } + + string getAction() { result = action } +} diff --git a/actions/ql/lib/ext/config/minimum_permissions_map.yml b/actions/ql/lib/ext/config/minimum_permissions_map.yml new file mode 100644 index 000000000000..f20fa1c4025f --- /dev/null +++ b/actions/ql/lib/ext/config/minimum_permissions_map.yml @@ -0,0 +1,297 @@ +extensions: + - addsTo: + pack: github/actions-all + extensible: minimumPermissionsDataModel + data: + - ["actions/cache", "{}"] + - ["actions/setup-node", "contents:read"] + - ["actions/upload-artifact", "{}"] + - ["actions/setup-python", "contents:read"] + - ["actions/download-artifact", "{}"] + - ["actions/github-script", "It depends on what the script does"] + - ["actions/setup-java", "contents:read"] + - ["actions/setup-go", "contents:read"] + - ["actions/setup-dotnet", "contents:read"] + - ["actions/labeler", "contents:read, pull-requests:write"] + - ["actions/attest", "id-token:write, attestations:write"] + - ["actions/add-to-project", "repository-projects:read, repository-projects:write, issues:read, pull-requests:read"] + - ["actions/dependency-review-action", "contents:read"] + - ["actions/attest-sbom", "id-token:write, attestations:write"] + - ["actions/stale", "contents:write, issues:write, pull-requests:write"] + - ["actions/attest-build-provenance", "id-token:write, attestations:write"] + - ["actions/jekyll-build-pages", "contents:read, pages:write, id-token:write"] + - ["actions/publish-action", "contents:write"] + - ["actions/version-package-tools", "contents:read, actions:read"] + - ["actions/reusable-workflows", "contents:read, actions:read"] + - ["actions/checkout", "contents:read" ] + - ["GET /repos/{owner}/{repo}/actions/artifacts", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}", "actions:read"] + - ["DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/cache/usage", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/caches", "actions:read"] + - ["DELETE /repos/{owner}/{repo}/actions/caches", "actions:write"] + - ["DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/oidc/customization/sub", "actions:read"] + - ["PUT /repos/{owner}/{repo}/actions/oidc/customization/sub", "actions:write"] + - ["PUT /orgs/{org}/actions/permissions/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/actions/permissions/repositories/{repository_id}", "metadata:read"] + - ["PUT /orgs/{org}/actions/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/actions/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["PUT /orgs/{org}/actions/runner-groups/{runner_group_id}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/actions/runner-groups/{runner_group_id}/repositories/{repository_id}", "metadata:read"] + - ["PUT /orgs/{org}/actions/variables/{name}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/actions/variables/{name}/repositories/{repository_id}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/actions/jobs/{job_id}", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs", "actions:read"] + - ["POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}", "actions:read"] + - ["DELETE /repos/{owner}/{repo}/actions/runs/{run_id}", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/approvals", "actions:read"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/approve", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/logs", "actions:read"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel", "actions:write"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs", "actions:read"] + - ["DELETE /repos/{owner}/{repo}/actions/runs/{run_id}/logs", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/pending_deployments", "actions:read"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/pending_deployments", "actions:read,checks:read,deployments:read"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun", "actions:write"] + - ["POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun-failed-jobs", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/runs/{run_id}/timing", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/workflows", "actions:read"] + - ["GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}", "actions:read"] + - ["PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable", "actions:write"] + - ["POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", "actions:write"] + - ["PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable", "actions:write"] + - ["GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/timing", "actions:read"] + - ["GET /repos/{owner}/{repo}/events", "metadata:read"] + - ["GET /repos/{owner}/{repo}/stargazers", "metadata:read"] + - ["GET /user/starred/{owner}/{repo}", "metadata:read"] + - ["PUT /user/starred/{owner}/{repo}", "metadata:read"] + - ["DELETE /user/starred/{owner}/{repo}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/subscribers", "metadata:read"] + - ["GET /repos/{owner}/{repo}/branches", "contents:read"] + - ["GET /repos/{owner}/{repo}/branches/{branch}", "contents:read"] + - ["POST /repos/{owner}/{repo}/branches/{branch}/rename", "contents:write"] + - ["POST /repos/{owner}/{repo}/merge-upstream", "contents:write"] + - ["POST /repos/{owner}/{repo}/merges", "contents:write"] + - ["POST /repos/{owner}/{repo}/check-runs", "checks:write"] + - ["GET /repos/{owner}/{repo}/check-runs/{check_run_id}", "checks:read"] + - ["PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", "checks:write"] + - ["GET /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations", "checks:read"] + - ["POST /repos/{owner}/{repo}/check-runs/{check_run_id}/rerequest", "checks:write"] + - ["GET /repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs", "checks:read"] + - ["GET /repos/{owner}/{repo}/commits/{ref}/check-runs", "checks:read"] + - ["POST /repos/{owner}/{repo}/check-suites", "checks:write"] + - ["PATCH /repos/{owner}/{repo}/check-suites/preferences", "checks:write"] + - ["GET /repos/{owner}/{repo}/check-suites/{check_suite_id}", "checks:read"] + - ["POST /repos/{owner}/{repo}/check-suites/{check_suite_id}/rerequest", "checks:write"] + - ["GET /repos/{owner}/{repo}/commits/{ref}/check-suites", "checks:read"] + - ["POST /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/autofix/commits", "contents:write"] + - ["GET /repos/{owner}/{repo}/code-scanning/codeql/databases", "contents:read"] + - ["GET /repos/{owner}/{repo}/code-scanning/codeql/databases/{language}", "contents:read"] + - ["DELETE /repos/{owner}/{repo}/code-scanning/codeql/databases/{language}", "contents:write"] + - ["POST /repos/{owner}/{repo}/code-scanning/codeql/variant-analyses", "contents:write"] + - ["GET /repos/{owner}/{repo}/code-scanning/codeql/variant-analyses/{codeql_variant_analysis_id}", "contents:read"] + - ["GET /repos/{owner}/{repo}/code-scanning/codeql/variant-analyses/{codeql_variant_analysis_id}/repos/{repo_owner}/{repo_name}", "contents:read"] + - ["PUT /orgs/{org}/codespaces/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/codespaces/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["PUT /user/codespaces/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /user/codespaces/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/collaborators", "metadata:read"] + - ["GET /repos/{owner}/{repo}/collaborators/{username}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/collaborators/{username}/permission", "metadata:read"] + - ["GET /repos/{owner}/{repo}/commits", "contents:read"] + - ["GET /repos/{owner}/{repo}/commits/{commit_sha}/branches-where-head", "contents:read"] + - ["GET /repos/{owner}/{repo}/commits/{ref}", "contents:read"] + - ["GET /repos/{owner}/{repo}/compare/{basehead}", "contents:read"] + - ["GET /repos/{owner}/{repo}/comments", "metadata:read"] + - ["GET /repos/{owner}/{repo}/comments/{comment_id}", "metadata:read"] + - ["PATCH /repos/{owner}/{repo}/comments/{comment_id}", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/comments/{comment_id}", "contents:write"] + - ["GET /repos/{owner}/{repo}/commits/{commit_sha}/comments", "metadata:read"] + - ["POST /repos/{owner}/{repo}/commits/{commit_sha}/comments", "contents:read"] + - ["PUT /orgs/{org}/dependabot/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["DELETE /orgs/{org}/dependabot/secrets/{secret_name}/repositories/{repository_id}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}", "contents:read"] + - ["POST /repos/{owner}/{repo}/dependency-graph/snapshots", "contents:write"] + - ["GET /repos/{owner}/{repo}/dependency-graph/sbom", "contents:read"] + - ["GET /repos/{owner}/{repo}/deployments", "deployments:read"] + - ["POST /repos/{owner}/{repo}/deployments", "deployments:write"] + - ["GET /repos/{owner}/{repo}/deployments/{deployment_id}", "deployments:read"] + - ["DELETE /repos/{owner}/{repo}/deployments/{deployment_id}", "deployments:write"] + - ["GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies", "actions:read"] + - ["GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}", "actions:read"] + - ["GET /repos/{owner}/{repo}/environments", "actions:read"] + - ["GET /repos/{owner}/{repo}/environments/{environment_name}", "actions:read"] + - ["GET /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules", "actions:read"] + - ["GET /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules/{protection_rule_id}", "actions:read"] + - ["GET /repos/{owner}/{repo}/deployments/{deployment_id}/statuses", "deployments:read"] + - ["POST /repos/{owner}/{repo}/deployments/{deployment_id}/statuses", "deployments:write"] + - ["GET /repos/{owner}/{repo}/deployments/{deployment_id}/statuses/{status_id}", "deployments:read"] + - ["POST /repos/{owner}/{repo}/git/blobs", "contents:write"] + - ["GET /repos/{owner}/{repo}/git/blobs/{file_sha}", "contents:read"] + - ["POST /repos/{owner}/{repo}/git/commits", "contents:write"] + - ["GET /repos/{owner}/{repo}/git/commits/{commit_sha}", "contents:read"] + - ["GET /repos/{owner}/{repo}/git/matching-refs/{ref}", "contents:read"] + - ["GET /repos/{owner}/{repo}/git/ref/{ref}", "contents:read"] + - ["POST /repos/{owner}/{repo}/git/refs", "contents:write"] + - ["PATCH /repos/{owner}/{repo}/git/refs/{ref}", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/git/refs/{ref}", "contents:write"] + - ["POST /repos/{owner}/{repo}/git/tags", "contents:write"] + - ["GET /repos/{owner}/{repo}/git/tags/{tag_sha}", "contents:read"] + - ["POST /repos/{owner}/{repo}/git/trees", "contents:write"] + - ["GET /repos/{owner}/{repo}/git/trees/{tree_sha}", "contents:read"] + - ["GET /repos/{owner}/{repo}/issues", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}", "issues:read"] + - ["PATCH /repos/{owner}/{repo}/issues/{issue_number}", "issues:write"] + - ["PUT /repos/{owner}/{repo}/issues/{issue_number}/lock", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/lock", "issues:write"] + - ["GET /repos/{owner}/{repo}/assignees", "issues:read"] + - ["GET /repos/{owner}/{repo}/assignees/{assignee}", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/assignees", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", "issues:read"] + - ["GET /repos/{owner}/{repo}/issues/comments", "issues:read"] + - ["GET /repos/{owner}/{repo}/issues/comments/{comment_id}", "issues:read"] + - ["PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/comments", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/{issue_number}/comments", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/events", "issues:read"] + - ["GET /repos/{owner}/{repo}/issues/events/{event_id}", "issues:read"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/events", "issues:read"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/labels", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/{issue_number}/labels", "issues:write"] + - ["PUT /repos/{owner}/{repo}/issues/{issue_number}/labels", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}", "issues:write"] + - ["GET /repos/{owner}/{repo}/labels", "issues:read"] + - ["POST /repos/{owner}/{repo}/labels", "issues:write"] + - ["GET /repos/{owner}/{repo}/labels/{name}", "issues:read"] + - ["PATCH /repos/{owner}/{repo}/labels/{name}", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/labels/{name}", "issues:write"] + - ["GET /repos/{owner}/{repo}/milestones/{milestone_number}/labels", "issues:read"] + - ["GET /repos/{owner}/{repo}/milestones", "issues:read"] + - ["POST /repos/{owner}/{repo}/milestones", "issues:write"] + - ["GET /repos/{owner}/{repo}/milestones/{milestone_number}", "issues:read"] + - ["PATCH /repos/{owner}/{repo}/milestones/{milestone_number}", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/milestones/{milestone_number}", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issue", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues", "issues:write"] + - ["PATCH /repos/{owner}/{repo}/issues/{issue_number}/sub_issues/priority", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/timeline", "issues:read"] + - ["GET /repos/{owner}/{repo}/license", "metadata:read"] + - ["GET /repos/{owner}/{repo}/community/profile", "contents:read"] + - ["GET /repos/{owner}/{repo}/stats/code_frequency", "metadata:read"] + - ["GET /repos/{owner}/{repo}/stats/commit_activity", "metadata:read"] + - ["GET /repos/{owner}/{repo}/stats/contributors", "metadata:read"] + - ["GET /repos/{owner}/{repo}/stats/participation", "metadata:read"] + - ["GET /repos/{owner}/{repo}/stats/punch_card", "metadata:read"] + - ["GET /repos/{owner}/{repo}/import", "contents:read"] + - ["PUT /repos/{owner}/{repo}/import", "contents:write"] + - ["PATCH /repos/{owner}/{repo}/import", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/import", "contents:write"] + - ["GET /repos/{owner}/{repo}/import/authors", "contents:read"] + - ["PATCH /repos/{owner}/{repo}/import/authors/{author_id}", "contents:write"] + - ["GET /repos/{owner}/{repo}/import/large_files", "contents:read"] + - ["PATCH /repos/{owner}/{repo}/import/lfs", "contents:write"] + - ["GET /repos/{owner}/{repo}/pages", "pages:read"] + - ["POST /repos/{owner}/{repo}/pages", "pages:write"] + - ["PUT /repos/{owner}/{repo}/pages", "pages:write"] + - ["DELETE /repos/{owner}/{repo}/pages", "pages:write"] + - ["GET /repos/{owner}/{repo}/pages/builds", "pages:read"] + - ["POST /repos/{owner}/{repo}/pages/builds", "pages:write"] + - ["GET /repos/{owner}/{repo}/pages/builds/latest", "pages:read"] + - ["GET /repos/{owner}/{repo}/pages/builds/{build_id}", "pages:read"] + - ["POST /repos/{owner}/{repo}/pages/deployments", "pages:write"] + - ["GET /repos/{owner}/{repo}/pages/deployments/{pages_deployment_id}", "pages:read"] + - ["POST /repos/{owner}/{repo}/pages/deployments/{pages_deployment_id}/cancel", "pages:write"] + - ["POST /repos/{owner}/{repo}/pages/health", "pages:write"] + - ["GET /orgs/{org}/projects", "repository-projects:read"] + - ["POST /orgs/{org}/projects", "repository-projects:write"] + - ["GET /projects/{project_id}", "repository-projects:read"] + - ["PATCH /projects/{project_id}", "repository-projects:write"] + - ["DELETE /projects/{project_id}", "repository-projects:write"] + - ["GET /repos/{owner}/{repo}/projects", "repository-projects:read"] + - ["POST /repos/{owner}/{repo}/projects", "repository-projects:write"] + - ["GET /projects/columns/cards/{card_id}", "repository-projects:read"] + - ["PATCH /projects/columns/cards/{card_id}", "repository-projects:write"] + - ["DELETE /projects/columns/cards/{card_id}", "repository-projects:write"] + - ["POST /projects/columns/cards/{card_id}/moves", "repository-projects:write"] + - ["GET /projects/columns/{column_id}/cards", "repository-projects:read"] + - ["POST /projects/columns/{column_id}/cards", "repository-projects:write"] + - ["GET /projects/{project_id}/collaborators", "repository-projects:write, repository-projects:admin"] + - ["PUT /projects/{project_id}/collaborators/{username}", "repository-projects:write, repository-projects:admin"] + - ["DELETE /projects/{project_id}/collaborators/{username}", "repository-projects:write, repository-projects:admin"] + - ["GET /projects/{project_id}/collaborators/{username}/permission", "repository-projects:write, repository-projects:admin"] + - ["GET /projects/columns/{column_id}", "repository-projects:read"] + - ["PATCH /projects/columns/{column_id}", "repository-projects:write"] + - ["DELETE /projects/columns/{column_id}", "repository-projects:write"] + - ["POST /projects/columns/{column_id}/moves", "repository-projects:write"] + - ["GET /projects/{project_id}/columns", "repository-projects:read"] + - ["POST /projects/{project_id}/columns", "repository-projects:write"] + - ["GET /repos/{owner}/{repo}/pulls/{pull_number}", "contents:read"] + - ["PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge", "contents:write"] + - ["GET /repos/{owner}/{repo}/comments/{comment_id}/reactions", "metadata:read"] + - ["POST /repos/{owner}/{repo}/comments/{comment_id}/reactions", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/comments/{comment_id}/reactions/{reaction_id}", "contents:write"] + - ["GET /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions/{reaction_id}", "issues:write"] + - ["GET /repos/{owner}/{repo}/issues/{issue_number}/reactions", "issues:read"] + - ["POST /repos/{owner}/{repo}/issues/{issue_number}/reactions", "issues:write"] + - ["DELETE /repos/{owner}/{repo}/issues/{issue_number}/reactions/{reaction_id}", "issues:write"] + - ["GET /repos/{owner}/{repo}/releases", "contents:read"] + - ["POST /repos/{owner}/{repo}/releases", "contents:write"] + - ["POST /repos/{owner}/{repo}/releases/generate-notes", "contents:write"] + - ["GET /repos/{owner}/{repo}/releases/latest", "contents:read"] + - ["GET /repos/{owner}/{repo}/releases/tags/{tag}", "contents:read"] + - ["GET /repos/{owner}/{repo}/releases/{release_id}", "contents:read"] + - ["PATCH /repos/{owner}/{repo}/releases/{release_id}", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/releases/{release_id}", "contents:write"] + - ["GET /repos/{owner}/{repo}/releases/assets/{asset_id}", "contents:read"] + - ["PATCH /repos/{owner}/{repo}/releases/assets/{asset_id}", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}", "contents:write"] + - ["GET /repos/{owner}/{repo}/releases/{release_id}/assets", "contents:read"] + - ["GET /orgs/{org}/repos", "metadata:read"] + - ["GET /repos/{owner}/{repo}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/activity", "contents:read"] + - ["POST /repos/{owner}/{repo}/attestations", "attestations:write"] + - ["GET /repos/{owner}/{repo}/attestations/{subject_digest}", "attestations:read"] + - ["GET /repos/{owner}/{repo}/codeowners/errors", "contents:read"] + - ["GET /repos/{owner}/{repo}/contributors", "metadata:read"] + - ["POST /repos/{owner}/{repo}/dispatches", "contents:write"] + - ["GET /repos/{owner}/{repo}/languages", "metadata:read"] + - ["GET /repos/{owner}/{repo}/private-vulnerability-reporting", "metadata:read"] + - ["GET /repos/{owner}/{repo}/tags", "metadata:read"] + - ["GET /repos/{owner}/{repo}/topics", "metadata:read"] + - ["GET /repositories", "metadata:read"] + - ["GET /user/repos", "metadata:read"] + - ["GET /users/{username}/repos", "metadata:read"] + - ["GET /repos/{owner}/{repo}/contents/{path}", "contents:read"] + - ["PUT /repos/{owner}/{repo}/contents/{path}", "contents:write"] + - ["DELETE /repos/{owner}/{repo}/contents/{path}", "contents:write"] + - ["GET /repos/{owner}/{repo}/tarball/{ref}", "contents:read"] + - ["GET /repos/{owner}/{repo}/zipball/{ref}", "contents:read"] + - ["GET /repos/{owner}/{repo}/properties/values", "metadata:read"] + - ["GET /repos/{owner}/{repo}/forks", "metadata:read"] + - ["POST /repos/{owner}/{repo}/forks", "contents:read"] + - ["GET /repos/{owner}/{repo}/rules/branches/{branch}", "metadata:read"] + - ["GET /repos/{owner}/{repo}/rulesets", "metadata:read"] + - ["GET /repos/{owner}/{repo}/rulesets/{ruleset_id}", "metadata:read"] + - ["GET /search/labels", "metadata:read"] + - ["POST /repos/{owner}/{repo}/secret-scanning/push-protection-bypasses", "contents:write"] + - ["GET /repos/{owner}/{repo}/commits/{ref}/status", "repository-projects:read"] + - ["POST /repos/{owner}/{repo}/statuses/{sha}", "repository-projects:write"] + - ["DELETE /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number}/reactions/{reaction_id}", "discussions:write"] + - ["DELETE /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions/{reaction_id}", "discussions:write"] \ No newline at end of file diff --git a/actions/ql/src/Security/CWE-275/MissingActionsPermissions.ql b/actions/ql/src/Security/CWE-275/MissingActionsPermissions.ql index d2969b7d6e72..acd160fe637b 100644 --- a/actions/ql/src/Security/CWE-275/MissingActionsPermissions.ql +++ b/actions/ql/src/Security/CWE-275/MissingActionsPermissions.ql @@ -11,15 +11,38 @@ * external/cwe/cwe-275 */ -import actions + import actions + import codeql.actions.security.MinimumActionsPermissions -from Job job -where - not exists(job.getPermissions()) and - not exists(job.getEnclosingWorkflow().getPermissions()) and - // exists a trigger event that is not a workflow_call - exists(Event e | - e = job.getATriggerEvent() and - not e.getName() = "workflow_call" - ) -select job, "Actions Job or Workflow does not set permissions" + // Returns the minimum permissions for all of the uses steps + // that are children of the job separated by a comma + // e.g. "contents: read, packages: write". If we cannot determine + // the permission we fallback to "unknown" + string getMinPermissions(Job job) { + if unknownPermissions(job) = true then result = "unknown" else + result = minPermissions(job) + } + + string minPermissions(Job job) { + result = concat(job.getAChildNode*().(MinimumActionsPermissions).getMinimumPermissions(), ", ") + } + + // Holds if we cannot determine the permissions for the uses step + // using the data extension or there are no uses steps + // that are children of the job + boolean unknownPermissions(Job job) { + minPermissions(job) = "" and result = true or count(job.getAChildNode*().(MinimumActionsPermissions)) = 0 and result = true + } + + from Job job + where + not exists(job.getPermissions()) and + not exists(job.getEnclosingWorkflow().getPermissions()) and + // exists a trigger event that is not a workflow_call + exists(Event e | + e = job.getATriggerEvent() and + not e.getName() = "workflow_call" + ) + select job, + "Actions Job or Workflow does not set permissions. Recommended minimum permissions are ($@)", + job, getMinPermissions(job) diff --git a/actions/ql/test/query-tests/Security/CWE-275/MissingActionsPermissions.expected b/actions/ql/test/query-tests/Security/CWE-275/MissingActionsPermissions.expected index 8f94d0dc45a6..2823f7d2a193 100644 --- a/actions/ql/test/query-tests/Security/CWE-275/MissingActionsPermissions.expected +++ b/actions/ql/test/query-tests/Security/CWE-275/MissingActionsPermissions.expected @@ -1,3 +1,3 @@ -| .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. Recommended minimum permissions are ($@) | .github/workflows/perms1.yml:6:5:9:32 | Job: build | contents:read | +| .github/workflows/perms2.yml:6:5:10:2 | Job: build | Actions Job or Workflow does not set permissions. Recommended minimum permissions are ($@) | .github/workflows/perms2.yml:6:5:10:2 | Job: build | contents:read | +| .github/workflows/perms5.yml:7:5:10:32 | Job: build | Actions Job or Workflow does not set permissions. Recommended minimum permissions are ($@) | .github/workflows/perms5.yml:7:5:10:32 | Job: build | contents:read |