@@ -317,6 +317,18 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
317
317
)
318
318
}
319
319
320
+ private predicate hasExplicitSecretAccess ( ) {
321
+ // the job accesses a secret other than GITHUB_TOKEN
322
+ exists ( SecretsExpressionImpl expr |
323
+ expr .getEnclosingCompositeAction ( ) = this and not expr .getFieldName ( ) = "GITHUB_TOKEN"
324
+ )
325
+ }
326
+
327
+ private predicate hasExplicitWritePermission ( ) {
328
+ // a calling job has an explicit write permission
329
+ this .getACaller ( ) .getPermissions ( ) .getAPermission ( ) .matches ( "%write" )
330
+ }
331
+
320
332
/** Holds if the action is privileged. */
321
333
predicate isPrivileged ( ) {
322
334
// there is a calling job that defines explicit write permissions
@@ -326,19 +338,24 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
326
338
this .hasExplicitSecretAccess ( )
327
339
or
328
340
// there is a privileged caller job
329
- this .getACaller ( ) .isPrivileged ( )
330
- }
331
-
332
- private predicate hasExplicitSecretAccess ( ) {
333
- // the job accesses a secret other than GITHUB_TOKEN
334
- exists ( SecretsExpressionImpl expr |
335
- expr .getEnclosingCompositeAction ( ) = this and not expr .getFieldName ( ) = "GITHUB_TOKEN"
341
+ (
342
+ this .getACaller ( ) .isPrivileged ( )
343
+ or
344
+ not this .getACaller ( ) .isPrivileged ( ) and
345
+ this .getACaller ( ) .getATriggerEvent ( ) .isPrivileged ( )
336
346
)
337
347
}
338
348
339
- private predicate hasExplicitWritePermission ( ) {
340
- // a calling job has an explicit write permission
341
- this .getACaller ( ) .getPermissions ( ) .getAPermission ( ) .matches ( "%write" )
349
+ /** Holds if the action is privileged and externally triggerable. */
350
+ predicate isPrivilegedExternallyTriggerable ( ) {
351
+ // the action is externally triggerable
352
+ exists ( JobImpl caller , EventImpl event |
353
+ caller = this .getACaller ( ) and
354
+ event = caller .getATriggerEvent ( ) and
355
+ event .isExternallyTriggerable ( ) and
356
+ // the action is privileged
357
+ ( this .isPrivileged ( ) or caller .isPrivileged ( ) )
358
+ )
342
359
}
343
360
}
344
361
@@ -688,6 +705,42 @@ class EventImpl extends AstNodeImpl, TEventNode {
688
705
689
706
/** Holds if the event has a property with the given name */
690
707
predicate hasProperty ( string prop ) { exists ( this .getAPropertyValue ( prop ) ) }
708
+
709
+ /** Holds if the event can be triggered by an external actor. */
710
+ predicate isExternallyTriggerable ( ) {
711
+ // the job is triggered by an event that can be triggered externally
712
+ externallyTriggerableEventsDataModel ( this .getName ( ) )
713
+ or
714
+ // the event is `workflow_call` and there is a caller workflow that can be triggered externally
715
+ this .getName ( ) = "workflow_call" and
716
+ (
717
+ // there are hints that this workflow is meant to be called by external triggers
718
+ exists ( ExpressionImpl expr , string external_trigger |
719
+ expr .getEnclosingWorkflow ( ) = this .getEnclosingWorkflow ( ) and
720
+ expr .getExpression ( ) .matches ( "%github.event" + external_trigger + "%" ) and
721
+ externallyTriggerableEventsDataModel ( external_trigger )
722
+ )
723
+ or
724
+ this .getEnclosingWorkflow ( )
725
+ .( ReusableWorkflowImpl )
726
+ .getACaller ( )
727
+ .getATriggerEvent ( )
728
+ .isExternallyTriggerable ( )
729
+ )
730
+ }
731
+
732
+ predicate isPrivileged ( ) {
733
+ // the Job is triggered by an event other than `pull_request`, or `workflow_call`
734
+ not this .getName ( ) = "pull_request" and
735
+ not this .getName ( ) = "workflow_call"
736
+ or
737
+ // Reusable Workflow with a privileged caller or we cant find a caller
738
+ this .getName ( ) = "workflow_call" and
739
+ (
740
+ this .getEnclosingWorkflow ( ) .( ReusableWorkflowImpl ) .getACaller ( ) .isPrivileged ( ) or
741
+ not exists ( this .getEnclosingWorkflow ( ) .( ReusableWorkflowImpl ) .getACaller ( ) )
742
+ )
743
+ }
691
744
}
692
745
693
746
class JobImpl extends AstNodeImpl , TJobNode {
@@ -746,45 +799,41 @@ class JobImpl extends AstNodeImpl, TJobNode {
746
799
/** Gets the strategy for this job. */
747
800
StrategyImpl getStrategy ( ) { result .getNode ( ) = n .lookup ( "strategy" ) }
748
801
749
- /** Holds if the job can be triggered by an external actor. */
750
- predicate isExternallyTriggerable ( ) {
751
- // the job is triggered by an event that can be triggered externally
752
- externallyTriggerableEventsDataModel ( this .getATriggerEvent ( ) .getName ( ) )
753
- or
754
- // the job is triggered by a workflow_call event that can be triggered externally
755
- this .getATriggerEvent ( ) .getName ( ) = "workflow_call" and
756
- (
757
- exists ( ExpressionImpl e , string external_trigger |
758
- e .getEnclosingJob ( ) = this and
759
- e .getExpression ( ) .matches ( "%github.event" + external_trigger + "%" ) and
760
- externallyTriggerableEventsDataModel ( external_trigger )
802
+ /** Gets the trigger event that starts this workflow. */
803
+ EventImpl getATriggerEvent ( ) { result = this .getEnclosingWorkflow ( ) .getATriggerEvent ( ) }
804
+
805
+ // private predicate hasSingleTrigger(string trigger) {
806
+ // this.getATriggerEvent().getName() = trigger and
807
+ // count(this.getATriggerEvent()) = 1
808
+ // }
809
+ /** Gets the runs-on field of the job. */
810
+ string getARunsOnLabel ( ) {
811
+ exists ( ScalarValueImpl lbl , YamlMappingLikeNode runson |
812
+ runson = n .lookup ( "runs-on" ) .( YamlMappingLikeNode )
813
+ |
814
+ (
815
+ lbl .getNode ( ) = runson .getNode ( _) and
816
+ not lbl .getNode ( ) = runson .getNode ( "group" )
817
+ or
818
+ lbl .getNode ( ) = runson .getNode ( "labels" ) .( YamlMappingLikeNode ) .getNode ( _)
819
+ ) and
820
+ (
821
+ not exists ( MatrixExpressionImpl e | e .getParentNode ( ) = lbl ) and
822
+ result =
823
+ lbl .getValue ( )
824
+ .trim ( )
825
+ .regexpReplaceAll ( "^('|\")" , "" )
826
+ .regexpReplaceAll ( "('|\")$" , "" )
827
+ .trim ( )
828
+ or
829
+ exists ( MatrixExpressionImpl e |
830
+ e .getParentNode ( ) = lbl and
831
+ result = e .getLiteralValues ( )
832
+ )
761
833
)
762
- or
763
- this .getEnclosingWorkflow ( ) .( ReusableWorkflowImpl ) .getACaller ( ) .isExternallyTriggerable ( )
764
834
)
765
835
}
766
836
767
- /** Holds if the job is privileged. */
768
- predicate isPrivileged ( ) {
769
- // the job has privileged runtime permissions
770
- this .hasRuntimeWritePermissions ( )
771
- or
772
- // the job has an explicit secret accesses
773
- this .hasExplicitSecretAccess ( )
774
- or
775
- // the job has an explicit write permission
776
- this .hasExplicitWritePermission ( )
777
- or
778
- // the job has no explicit permissions but the workflow has write permissions
779
- not exists ( this .getPermissions ( ) ) and
780
- this .hasImplicitWritePermission ( )
781
- or
782
- // neither the job nor the workflow have permissions but the job has a privileged trigger
783
- not exists ( this .getPermissions ( ) ) and
784
- not exists ( this .getEnclosingWorkflow ( ) .getPermissions ( ) ) and
785
- this .hasPrivilegedTrigger ( )
786
- }
787
-
788
837
private predicate hasExplicitSecretAccess ( ) {
789
838
// the job accesses a secret other than GITHUB_TOKEN
790
839
exists ( SecretsExpressionImpl expr |
@@ -817,60 +866,34 @@ class JobImpl extends AstNodeImpl, TJobNode {
817
866
)
818
867
}
819
868
820
- private predicate hasPrivilegedTrigger ( ) {
821
- // the Job is triggered by an event other than `pull_request`, `push`, or `workflow_call`
822
- count ( this .getATriggerEvent ( ) ) = 1 and
823
- not this .getATriggerEvent ( ) .getName ( ) = "push" and
824
- not this .getATriggerEvent ( ) .getName ( ) = "pull_request" and
825
- not this .getATriggerEvent ( ) .getName ( ) = "workflow_call"
869
+ /** Holds if the job is privileged. */
870
+ predicate isPrivileged ( ) {
871
+ // the job has privileged runtime permissions
872
+ this .hasRuntimeWritePermissions ( )
826
873
or
827
- // the Workflow is a Reusable Workflow only and there is
828
- // a privileged caller workflow or we cant find a caller
829
- count ( this .getATriggerEvent ( ) ) = 1 and
830
- this .getATriggerEvent ( ) .getName ( ) = "workflow_call" and
831
- (
832
- this .getEnclosingWorkflow ( ) .( ReusableWorkflowImpl ) .getACaller ( ) .isPrivileged ( ) or
833
- not exists ( this .getEnclosingWorkflow ( ) .( ReusableWorkflowImpl ) .getACaller ( ) )
834
- )
874
+ // the job has an explicit secret accesses
875
+ this .hasExplicitSecretAccess ( )
835
876
or
836
- // the Job is triggered by an event other than `push`, `pull_request`, or `workflow_call`
837
- exists ( string event |
838
- this .getATriggerEvent ( ) .getName ( ) = event and
839
- not event = [ "push" , "pull_request" , "workflow_call" ]
840
- )
877
+ // the job has an explicit write permission
878
+ this .hasExplicitWritePermission ( )
879
+ or
880
+ // the job has no explicit permissions but the workflow has write permissions
881
+ not exists ( this .getPermissions ( ) ) and
882
+ this .hasImplicitWritePermission ( )
841
883
}
842
884
843
- /** Gets the trigger event that starts this workflow. */
844
- EventImpl getATriggerEvent ( ) { result = this .getEnclosingWorkflow ( ) .getATriggerEvent ( ) }
845
-
846
- // private predicate hasSingleTrigger(string trigger) {
847
- // this.getATriggerEvent().getName() = trigger and
848
- // count(this.getATriggerEvent()) = 1
849
- // }
850
- /** Gets the runs-on field of the job. */
851
- string getARunsOnLabel ( ) {
852
- exists ( ScalarValueImpl lbl , YamlMappingLikeNode runson |
853
- runson = n .lookup ( "runs-on" ) .( YamlMappingLikeNode )
854
- |
885
+ /** Holds if the action is privileged and externally triggerable. */
886
+ predicate isPrivilegedExternallyTriggerable ( ) {
887
+ exists ( EventImpl e |
888
+ // job is triggereable by an external user
889
+ this .getATriggerEvent ( ) = e and
890
+ e .isExternallyTriggerable ( ) and
891
+ // job is privileged (write access or access to secrets)
855
892
(
856
- lbl .getNode ( ) = runson .getNode ( _) and
857
- not lbl .getNode ( ) = runson .getNode ( "group" )
893
+ this .isPrivileged ( )
858
894
or
859
- lbl .getNode ( ) = runson .getNode ( "labels" ) .( YamlMappingLikeNode ) .getNode ( _)
860
- ) and
861
- (
862
- not exists ( MatrixExpressionImpl e | e .getParentNode ( ) = lbl ) and
863
- result =
864
- lbl .getValue ( )
865
- .trim ( )
866
- .regexpReplaceAll ( "^('|\")" , "" )
867
- .regexpReplaceAll ( "('|\")$" , "" )
868
- .trim ( )
869
- or
870
- exists ( MatrixExpressionImpl e |
871
- e .getParentNode ( ) = lbl and
872
- result = e .getLiteralValues ( )
873
- )
895
+ not this .isPrivileged ( ) and
896
+ e .isPrivileged ( )
874
897
)
875
898
)
876
899
}
0 commit comments