@@ -64,7 +64,7 @@ private newtype TAstNode =
64
64
TInputsNode ( YamlMapping n ) { exists ( YamlMapping m | m .lookup ( "inputs" ) = n ) } or
65
65
TInputNode ( YamlValue n ) { exists ( YamlMapping m | m .lookup ( "inputs" ) .( YamlMapping ) .maps ( n , _) ) } or
66
66
TOutputsNode ( YamlMapping n ) { exists ( YamlMapping m | m .lookup ( "outputs" ) = n ) } or
67
- TPermissionsNode ( YamlMapping n ) { exists ( YamlMapping m | m .lookup ( "permissions" ) = n ) } or
67
+ TPermissionsNode ( YamlMappingLikeNode n ) { exists ( YamlMapping m | m .lookup ( "permissions" ) = n ) } or
68
68
TStrategyNode ( YamlMapping n ) { exists ( YamlMapping m | m .lookup ( "strategy" ) = n ) } or
69
69
TNeedsNode ( YamlMappingLikeNode n ) { exists ( YamlMapping m | m .lookup ( "needs" ) = n ) } or
70
70
TJobNode ( YamlMapping n ) { exists ( YamlMapping w | w .lookup ( "jobs" ) .( YamlMapping ) .lookup ( _) = n ) } or
@@ -320,6 +320,9 @@ class WorkflowImpl extends AstNodeImpl, TWorkflowNode {
320
320
/** Gets a job within this workflow */
321
321
JobImpl getAJob ( ) { result = this .getJob ( _) }
322
322
323
+ /** Gets the permissions granted to this workflow. */
324
+ PermissionsImpl getPermissions ( ) { result .getNode ( ) = n .lookup ( "permissions" ) }
325
+
323
326
/** Workflow is triggered by given trigger event */
324
327
predicate hasTriggerEvent ( string trigger ) {
325
328
exists ( YamlNode y | y = n .lookup ( "on" ) .( YamlMappingLikeNode ) .getNode ( trigger ) )
@@ -330,43 +333,8 @@ class WorkflowImpl extends AstNodeImpl, TWorkflowNode {
330
333
exists ( YamlNode y | y = n .lookup ( "on" ) .( YamlMappingLikeNode ) .getNode ( result ) )
331
334
}
332
335
333
- /** Gets the permissions granted to this workflow. */
334
- PermissionsImpl getPermissions ( ) { result .getNode ( ) = n .lookup ( "permissions" ) }
335
-
336
- private predicate hasSingleTrigger ( string trigger ) {
337
- this .getATriggerEvent ( ) = trigger and
338
- count ( this .getATriggerEvent ( ) ) = 1
339
- }
340
-
341
336
/** Gets the strategy for this workflow. */
342
337
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
- }
370
338
}
371
339
372
340
class ReusableWorkflowImpl extends AstNodeImpl , WorkflowImpl {
@@ -502,7 +470,7 @@ class OutputsImpl extends AstNodeImpl, TOutputsNode {
502
470
}
503
471
504
472
class PermissionsImpl extends AstNodeImpl , TPermissionsNode {
505
- YamlMapping n ;
473
+ YamlMappingLikeNode n ;
506
474
507
475
PermissionsImpl ( ) { this = TPermissionsNode ( n ) }
508
476
@@ -516,11 +484,41 @@ class PermissionsImpl extends AstNodeImpl, TPermissionsNode {
516
484
517
485
override Location getLocation ( ) { result = n .getLocation ( ) }
518
486
519
- override YamlMapping getNode ( ) { result = n }
487
+ override YamlMappingLikeNode getNode ( ) { result = n }
488
+
489
+ string getAScope ( ) {
490
+ result =
491
+ [
492
+ "actions" , "attestations" , "checks" , "contents" , "deployments" , "discussions" , "id-token" ,
493
+ "issues" , "packages" , "pages" , "pull-requests" , "repository-projects" , "security-events" ,
494
+ "statuses"
495
+ ]
496
+ }
520
497
521
- string getPermission ( string perm ) { result = n .lookup ( perm ) .( YamlScalar ) .getValue ( ) }
498
+ string getAPermission ( ) {
499
+ exists ( YamlMapping mapping , string scope |
500
+ mapping = n and
501
+ result = scope + ": " + mapping .lookup ( scope ) .( YamlScalar ) .getValue ( )
502
+ )
503
+ or
504
+ exists ( YamlScalar scalar |
505
+ scalar = n and
506
+ (
507
+ scalar .getValue ( ) = "write-all" and
508
+ result = this .getAScope ( ) + ":write"
509
+ or
510
+ scalar .getValue ( ) = "read-all" and
511
+ result = this .getAScope ( ) + ":read"
512
+ )
513
+ )
514
+ }
522
515
523
- string getAPermission ( ) { result = this .getPermission ( _) }
516
+ bindingset [ perm]
517
+ string getPermission ( string perm ) {
518
+ exists ( string p |
519
+ p = this .getAPermission ( ) and p .matches ( perm + ":%" ) and result = p .splitAt ( ":" , 1 ) .trim ( )
520
+ )
521
+ }
524
522
}
525
523
526
524
class StrategyImpl extends AstNodeImpl , TStrategyNode {
@@ -633,37 +631,87 @@ class JobImpl extends AstNodeImpl, TJobNode {
633
631
/** Gets the strategy for this job. */
634
632
StrategyImpl getStrategy ( ) { result .getNode ( ) = n .lookup ( "strategy" ) }
635
633
636
- /** Holds if the workflow is privileged. */
634
+ /** Holds if the job is privileged. */
637
635
predicate isPrivileged ( ) {
636
+ // the job has privileged runtime permissions
637
+ this .hasRuntimeWritePermissions ( )
638
+ or
639
+ // the job has an explicit secret accesses
640
+ this .hasExplicitSecretAccess ( )
641
+ or
638
642
// the job has an explicit write permission
639
- this .getPermissions ( ) .getAPermission ( ) = "write"
643
+ this .hasExplicitWritePermission ( )
644
+ or
645
+ // the job has no explicit permissions but the workflow has write permissions
646
+ not exists ( this .getPermissions ( ) ) and
647
+ this .hasImplicitWritePermission ( )
640
648
or
649
+ // neither the job nor the workflow have permissions but the job has a privileged trigger
650
+ not exists ( this .getPermissions ( ) ) and
651
+ not exists ( this .getEnclosingWorkflow ( ) .getPermissions ( ) ) and
652
+ this .hasPrivilegedTrigger ( )
653
+ }
654
+
655
+ private predicate hasExplicitSecretAccess ( ) {
641
656
// the job accesses a secret other than GITHUB_TOKEN
642
657
exists ( SecretsExpressionImpl expr |
643
658
expr .getEnclosingJob ( ) = this and not expr .getFieldName ( ) = "GITHUB_TOKEN"
644
659
)
645
- or
646
- // the effective permissions have write access
660
+ }
661
+
662
+ private predicate hasExplicitWritePermission ( ) {
663
+ // the job has an explicit write permission
664
+ this .getPermissions ( ) .getAPermission ( ) .matches ( "%write" )
665
+ }
666
+
667
+ private predicate hasImplicitWritePermission ( ) {
668
+ // the job has an explicit write permission
669
+ this .getEnclosingWorkflow ( ) .getPermissions ( ) .getAPermission ( ) .matches ( "%write" )
670
+ }
671
+
672
+ private predicate hasRuntimeWritePermissions ( ) {
673
+ // the effective runtime permissions have write access
647
674
exists ( string path , string trigger , string name , string secrets_source , string perms |
648
675
workflowDataModel ( path , trigger , name , secrets_source , perms , _) and
649
676
path .trim ( ) = this .getLocation ( ) .getFile ( ) .getRelativePath ( ) and
650
677
name .trim ( ) .matches ( this .getId ( ) + "%" ) and
651
678
// We cannot trust the permissions for pull_request events since they depend on the
652
- // location of the head branch
679
+ // provenance of the head branch (local vs fork)
653
680
not trigger .trim ( ) = "pull_request" and
654
- (
655
- secrets_source .trim ( ) .toLowerCase ( ) = "actions" or
656
- perms .toLowerCase ( ) .matches ( "%write%" )
657
- )
681
+ perms .toLowerCase ( ) .matches ( "%write%" )
658
682
)
683
+ }
684
+
685
+ private predicate hasPrivilegedTrigger ( ) {
686
+ // For workflows that are triggered by the pull_request_target event, the GITHUB_TOKEN is granted read/write repository permission unless the permissions key is specified and the workflow can access secrets, even when it is triggered from a fork.
687
+ // The Job is triggered by an event other than `pull_request`
688
+ count ( this .getATriggerEvent ( ) ) = 1 and
689
+ not this .getATriggerEvent ( ) = [ "pull_request" , "workflow_call" ]
659
690
or
660
- // The job has no expliclit permission, but the enclosing workflow is privileged
661
- not exists ( this .getPermissions ( ) ) and
662
- not exists ( SecretsExpressionImpl expr |
663
- expr .getEnclosingJob ( ) = this and not expr .getFieldName ( ) = "GITHUB_TOKEN"
664
- ) and
665
- // The enclosing workflow is privileged
666
- this .getEnclosingWorkflow ( ) .isPrivileged ( )
691
+ // The Workflow is only triggered by `workflow_call` and there is
692
+ // a caller workflow triggered by an event other than `pull_request`
693
+ this .hasSingleTrigger ( "workflow_call" ) and
694
+ exists ( ExternalJobImpl call , JobImpl caller |
695
+ call .getCallee ( ) = this .getLocation ( ) .getFile ( ) .getRelativePath ( ) and
696
+ caller = call .getEnclosingJob ( ) and
697
+ caller .isPrivileged ( )
698
+ )
699
+ or
700
+ // The Workflow has multiple triggers so at least one is not "pull_request"
701
+ count ( this .getATriggerEvent ( ) ) > 1
702
+ }
703
+
704
+ /** Workflow is triggered by given trigger event */
705
+ predicate hasTriggerEvent ( string trigger ) {
706
+ exists ( YamlNode y | y = n .lookup ( "on" ) .( YamlMappingLikeNode ) .getNode ( trigger ) )
707
+ }
708
+
709
+ /** Gets the trigger event that starts this workflow. */
710
+ string getATriggerEvent ( ) { result = this .getEnclosingWorkflow ( ) .getATriggerEvent ( ) }
711
+
712
+ private predicate hasSingleTrigger ( string trigger ) {
713
+ this .getATriggerEvent ( ) = trigger and
714
+ count ( this .getATriggerEvent ( ) ) = 1
667
715
}
668
716
669
717
/** Gets the runs-on field of the job. */
@@ -827,11 +875,14 @@ class UsesStepImpl extends StepImpl, UsesImpl {
827
875
828
876
/** Gets the owner and name of the repository where the Action comes from, e.g. `actions/checkout` in `actions/checkout@v2`. */
829
877
override string getCallee ( ) {
830
- result =
831
- (
832
- u .getValue ( ) .regexpCapture ( usesParser ( ) , 1 ) + "/" +
833
- u .getValue ( ) .regexpCapture ( usesParser ( ) , 2 )
834
- ) .toLowerCase ( )
878
+ if u .getValue ( ) .matches ( "./%" )
879
+ then result = u .getValue ( )
880
+ else
881
+ result =
882
+ (
883
+ u .getValue ( ) .regexpCapture ( usesParser ( ) , 1 ) + "/" +
884
+ u .getValue ( ) .regexpCapture ( usesParser ( ) , 2 )
885
+ ) .toLowerCase ( )
835
886
}
836
887
837
888
/** Gets the version reference used when checking out the Action, e.g. `2` in `actions/checkout@v2`. */
0 commit comments