11import actions
22
3+ string any_relevant_category ( ) {
4+ result =
5+ [
6+ "untrusted-checkout" , "output-clobbering" , "envpath-injection" , "envvar-injection" ,
7+ "command-injection" , "argument-injection" , "code-injection" , "cache-poisoning" ,
8+ "untrusted-checkout-toctou" , "artifact-poisoning"
9+ ]
10+ }
11+
12+ string any_non_toctou_category ( ) {
13+ result = any_relevant_category ( ) and not result = "untrusted-checkout-toctou"
14+ }
15+
16+ string any_relevant_event ( ) {
17+ result =
18+ [
19+ "pull_request_target" ,
20+ "issue_comment" ,
21+ "pull_request_comment" ,
22+ "workflow_run" ,
23+ "issues" ,
24+ "fork" ,
25+ "watch" ,
26+ "discussion_comment" ,
27+ "discussion"
28+ ]
29+ }
30+
331/** An If node that contains an actor, user or label check */
432abstract class ControlCheck extends AstNode {
533 ControlCheck ( ) {
634 this instanceof If or
735 this instanceof Environment or
8- this instanceof UsesStep
36+ this instanceof UsesStep or
37+ this instanceof Run
938 }
1039
11- predicate protects ( Step step , Event event ) {
40+ predicate protects ( Step step , Event event , string category ) {
1241 event .getEnclosingWorkflow ( ) = step .getEnclosingWorkflow ( ) and
13- this .getAProtectedEvent ( ) = event . getName ( ) and
14- this .dominates ( step )
42+ this .dominates ( step ) and
43+ this .protectsCategoryAndEvent ( category , event . getName ( ) )
1544 }
1645
1746 predicate dominates ( Step step ) {
@@ -30,80 +59,71 @@ abstract class ControlCheck extends AstNode {
3059 step .getEnclosingJob ( ) .getANeededJob ( ) .getEnvironment ( ) = this
3160 )
3261 or
33- this .( UsesStep ) .getAFollowingStep ( ) = step
62+ this .( Step ) .getAFollowingStep ( ) = step
3463 }
3564
36- abstract string getAProtectedEvent ( ) ;
37-
38- abstract boolean protectsAgainstRefMutationAttacks ( ) ;
65+ abstract predicate protectsCategoryAndEvent ( string category , string event ) ;
3966}
4067
4168abstract class AssociationCheck extends ControlCheck {
42- // checks who you are (identity)
43- // association checks are effective against pull requests since they can control who is making the PR
44- // they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
45- // someone entitled to trigger the workflow with a comment, may no detect a malicious comment, or the comment may mutate after approval
46- override string getAProtectedEvent ( ) { result = [ "pull_request" , "pull_request_target" ] }
47-
48- override boolean protectsAgainstRefMutationAttacks ( ) { result = true }
69+ // Checks if the actor is a MEMBER/OWNER the repo
70+ // - they are effective against pull requests and workflow_run (since these are triggered by pull_requests) since they can control who is making the PR
71+ // - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
72+ override predicate protectsCategoryAndEvent ( string category , string event ) {
73+ event = [ "pull_request_target" , "workflow_run" ] and category = any_relevant_category ( )
74+ }
4975}
5076
5177abstract class ActorCheck extends ControlCheck {
52- // checks who you are (identity)
53- // actor checks are effective against pull requests since they can control who is making the PR
54- // they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
55- // someone entitled to trigger the workflow with a comment, may no detect a malicious comment, or the comment may mutate after approval
56- override string getAProtectedEvent ( ) { result = [ "pull_request" , "pull_request_target" ] }
57-
58- override boolean protectsAgainstRefMutationAttacks ( ) { result = true }
78+ // checks for a specific actor
79+ // - they are effective against pull requests and workflow_run (since these are triggered by pull_requests) since they can control who is making the PR
80+ // - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
81+ override predicate protectsCategoryAndEvent ( string category , string event ) {
82+ event = [ "pull_request_target" , "workflow_run" ] and category = any_relevant_category ( )
83+ }
5984}
6085
6186abstract class RepositoryCheck extends ControlCheck {
62- // repository checks are effective against pull requests since they can control where the code is coming from
63- // they are not effective against issue_comment since the repository will always be the same
64- // who you are (identity)
65- override string getAProtectedEvent ( ) { result = [ "pull_request" , "pull_request_target" ] }
66-
67- override boolean protectsAgainstRefMutationAttacks ( ) { result = true }
87+ // checks that the origin of the code is the same as the repository.
88+ // for pull_requests, that means that it triggers only on local branches or repos from the same org
89+ // - they are effective against pull requests/workflow_run since they can control where the code is coming from
90+ // - they are not effective against issue_comment since the repository will always be the same
91+ override predicate protectsCategoryAndEvent ( string category , string event ) {
92+ event = [ "pull_request_target" , "workflow_run" ] and category = any_relevant_category ( )
93+ }
6894}
6995
7096abstract class PermissionCheck extends ControlCheck {
71- // permission checks are effective against pull requests since they can control who can make changes
72- // they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
73- // someone entitled to trigger the workflow with a comment, may no detect a malicious comment, or the comment may mutate after approval
74- // who you are (identity)
75- override string getAProtectedEvent ( ) { result = [ "pull_request " , "pull_request_target" ] }
76-
77- override boolean protectsAgainstRefMutationAttacks ( ) { result = true }
97+ // checks that the actor has a specific permission level
98+ // - they are effective against pull requests/workflow_run since they can control who can make changes
99+ // - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
100+ override predicate protectsCategoryAndEvent ( string category , string event ) {
101+ event = [ "pull_request_target " , "workflow_run" , "issue_comment" ] and
102+ category = any_relevant_category ( )
103+ }
78104}
79105
80106abstract class LabelCheck extends ControlCheck {
81- // does it protect injection attacks but not pwn requests?
82- // pwn requests are susceptible to checkout of mutable code
83- // but injection attacks are not, although a branch name can be changed after approval and perhaps also some other things
84- // they do actually protext against untrusted code execution (sha)
85- // what you have (approval)
86- // TODO: A check should be a combination of:
87- // - event type (pull_request, issue_comment, etc)
88- // - category (untrusted mutable code, untrusted immutable code, code injection, etc)
89- // - we dont know this unless we pass category to inPrivilegedContext and into ControlCheck.protects
90- // - we can decide if a control check is effective based only on the ast node
91- override string getAProtectedEvent ( ) { result = [ "pull_request" , "pull_request_target" ] }
92-
93- // ref can be mutated after approval
94- override boolean protectsAgainstRefMutationAttacks ( ) { result = false }
107+ // checks if the issue/pull_request is labeled, which implies that it could have been approved
108+ // - they dont protect against mutation attacks
109+ override predicate protectsCategoryAndEvent ( string category , string event ) {
110+ event = [ "pull_request_target" , "workflow_run" ] and category = any_non_toctou_category ( )
111+ }
95112}
96113
97114class EnvironmentCheck extends ControlCheck instanceof Environment {
98115 // Environment checks are not effective against any mutable attacks
99- // they do actually protext against untrusted code execution (sha)
100- // what you have (approval)
101- EnvironmentCheck ( ) { any ( ) }
102-
103- override string getAProtectedEvent ( ) { result = [ "pull_request" , "pull_request_target" ] }
116+ // they do actually protect against untrusted code execution (sha)
117+ override predicate protectsCategoryAndEvent ( string category , string event ) {
118+ event = [ "pull_request_target" , "workflow_run" ] and category = any_non_toctou_category ( )
119+ }
120+ }
104121
105- // ref can be mutated after approval
106- override boolean protectsAgainstRefMutationAttacks ( ) { result = false }
122+ abstract class CommentVsHeadDateCheck extends ControlCheck {
123+ override predicate protectsCategoryAndEvent ( string category , string event ) {
124+ // by itself, this check is not effective against any attacks
125+ none ( )
126+ }
107127}
108128
109129/* Specific implementations of control checks */
@@ -162,28 +182,37 @@ class RepositoryIfCheck extends RepositoryCheck instanceof If {
162182class AssociationIfCheck extends AssociationCheck instanceof If {
163183 AssociationIfCheck ( ) {
164184 // eg: contains(fromJson('["MEMBER", "OWNER"]'), github.event.comment.author_association)
165- exists (
166- normalizeExpr ( this .getCondition ( ) )
167- .regexpFind ( [
168- "\\bgithub\\.event\\.comment\\.author_association\\b" ,
169- "\\bgithub\\.event\\.issue\\.author_association\\b" ,
170- "\\bgithub\\.event\\.pull_request\\.author_association\\b" ,
171- ] , _, _)
172- )
185+ normalizeExpr ( this .getCondition ( ) )
186+ .splitAt ( "\n" )
187+ .regexpMatch ( [
188+ ".*\\bgithub\\.event\\.comment\\.author_association\\b.*" ,
189+ ".*\\bgithub\\.event\\.issue\\.author_association\\b.*" ,
190+ ".*\\bgithub\\.event\\.pull_request\\.author_association\\b.*" ,
191+ ] ) and
192+ normalizeExpr ( this .getCondition ( ) ) .splitAt ( "\n" ) .regexpMatch ( ".*\\bMEMBER\\b.*" ) and
193+ normalizeExpr ( this .getCondition ( ) ) .splitAt ( "\n" ) .regexpMatch ( ".*\\bOWNER\\b.*" )
173194 }
174195}
175196
176197class AssociationActionCheck extends AssociationCheck instanceof UsesStep {
177198 AssociationActionCheck ( ) {
178199 this .getCallee ( ) = "TheModdingInquisition/actions-team-membership" and
179- not exists ( this .getArgument ( "exit" ) )
180- or
181- this .getArgument ( "exit" ) = "true"
200+ (
201+ not exists ( this .getArgument ( "exit" ) )
202+ or
203+ this .getArgument ( "exit" ) = "true"
204+ )
182205 }
183206}
184207
185208class PermissionActionCheck extends PermissionCheck instanceof UsesStep {
186209 PermissionActionCheck ( ) {
210+ this .getCallee ( ) = "sushichop/action-repository-permission" and
211+ this .getArgument ( "required-permission" ) = [ "write" , "admin" ]
212+ or
213+ this .getCallee ( ) = "prince-chrismc/check-actor-permissions-action" and
214+ this .getArgument ( "permission" ) = [ "write" , "admin" ]
215+ or
187216 this .getCallee ( ) = "lannonbr/repo-permission-check-action" and
188217 this .getArgument ( "permission" ) = [ "write" , "admin" ]
189218 or
@@ -195,3 +224,13 @@ class PermissionActionCheck extends PermissionCheck instanceof UsesStep {
195224 )
196225 }
197226}
227+
228+ class BashCommentVsHeadDateCheck extends CommentVsHeadDateCheck , Run {
229+ BashCommentVsHeadDateCheck ( ) {
230+ exists ( string line |
231+ line = this .getScript ( ) .splitAt ( "\n" ) and
232+ line .toLowerCase ( )
233+ .regexpMatch ( ".*date\\s+-d.*(commit_at|pushed_at|comment_at|commented_at).*date\\s+-d.*(commit_at|pushed_at|comment_at|commented_at).*" )
234+ )
235+ }
236+ }
0 commit comments