|
1 | 1 | private import codeql.actions.ast.internal.Yaml |
2 | 2 | private import codeql.Locations |
3 | 3 | private import codeql.actions.Ast::Utils as Utils |
| 4 | +private import codeql.actions.dataflow.ExternalFlow |
4 | 5 |
|
5 | 6 | /** |
6 | 7 | * Gets the length of each line in the StringValue . |
@@ -332,8 +333,40 @@ class WorkflowImpl extends AstNodeImpl, TWorkflowNode { |
332 | 333 | /** Gets the permissions granted to this workflow. */ |
333 | 334 | PermissionsImpl getPermissions() { result.getNode() = n.lookup("permissions") } |
334 | 335 |
|
| 336 | + private predicate hasSingleTrigger(string trigger) { |
| 337 | + this.getATriggerEvent() = trigger and |
| 338 | + count(this.getATriggerEvent()) = 1 |
| 339 | + } |
| 340 | + |
335 | 341 | /** Gets the strategy for this workflow. */ |
336 | 342 | StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") } |
| 343 | + |
| 344 | + /** Holds if the workflow is privileged. */ |
| 345 | + predicate isPrivileged() { |
| 346 | + // The Workflow has a permission to write to some scope |
| 347 | + this.getPermissions().getAPermission() = "write" |
| 348 | + or |
| 349 | + // The Workflow accesses a secret |
| 350 | + exists(SecretsExpressionImpl expr | |
| 351 | + expr.getEnclosingWorkflow() = this and not expr.getFieldName() = "GITHUB_TOKEN" |
| 352 | + ) |
| 353 | + or |
| 354 | + // The Workflow is triggered by an event other than `pull_request` |
| 355 | + count(this.getATriggerEvent()) = 1 and |
| 356 | + not this.getATriggerEvent() = ["pull_request", "workflow_call"] |
| 357 | + or |
| 358 | + // The Workflow is only triggered by `workflow_call` and there is |
| 359 | + // a caller workflow triggered by an event other than `pull_request` |
| 360 | + this.hasSingleTrigger("workflow_call") and |
| 361 | + exists(ExternalJobImpl call, WorkflowImpl caller | |
| 362 | + call.getCallee() = this.getLocation().getFile().getRelativePath() and |
| 363 | + caller = call.getWorkflow() and |
| 364 | + caller.isPrivileged() |
| 365 | + ) |
| 366 | + or |
| 367 | + // The Workflow has multiple triggers so at least one is not "pull_request" |
| 368 | + count(this.getATriggerEvent()) > 1 |
| 369 | + } |
337 | 370 | } |
338 | 371 |
|
339 | 372 | class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl { |
@@ -597,6 +630,36 @@ class JobImpl extends AstNodeImpl, TJobNode { |
597 | 630 |
|
598 | 631 | /** Gets the strategy for this job. */ |
599 | 632 | StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") } |
| 633 | + |
| 634 | + /** Holds if the workflow is privileged. */ |
| 635 | + predicate isPrivileged() { |
| 636 | + // The job has a permission to write to some scope |
| 637 | + this.getPermissions().getAPermission() = "write" |
| 638 | + or |
| 639 | + // The job accesses a secret |
| 640 | + exists(SecretsExpressionImpl expr | |
| 641 | + expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN" |
| 642 | + ) |
| 643 | + or |
| 644 | + // The effective permissions have write access |
| 645 | + exists(string path, string name, string secrets_source, string perms | |
| 646 | + workflowDataModel(path, _, name, secrets_source, perms, _) and |
| 647 | + path.trim() = this.getLocation().getFile().getRelativePath() and |
| 648 | + name.trim().matches(this.getId() + "%") and |
| 649 | + ( |
| 650 | + secrets_source.trim().toLowerCase() = "actions" or |
| 651 | + perms.toLowerCase().matches("%write%") |
| 652 | + ) |
| 653 | + ) |
| 654 | + or |
| 655 | + // The job has no expliclit permission, but the enclosing workflow is privileged |
| 656 | + not exists(this.getPermissions()) and |
| 657 | + not exists(SecretsExpressionImpl expr | |
| 658 | + expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN" |
| 659 | + ) and |
| 660 | + // The enclosing workflow is privileged |
| 661 | + this.getEnclosingWorkflow().isPrivileged() |
| 662 | + } |
600 | 663 | } |
601 | 664 |
|
602 | 665 | class LocalJobImpl extends JobImpl { |
|
0 commit comments