@@ -34,8 +34,75 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
34
34
JobStmt getAJob ( ) { result = super .getJob ( _) }
35
35
36
36
JobStmt getJob ( string id ) { result = super .getJob ( id ) }
37
+
38
+ predicate isReusable ( ) { this instanceof ReusableWorkflowStmt }
39
+ }
40
+
41
+ class ReusableWorkflowStmt extends WorkflowStmt {
42
+ YamlValue workflow_call ;
43
+
44
+ ReusableWorkflowStmt ( ) {
45
+ this .( Actions:: Workflow ) .getOn ( ) .getNode ( "workflow_call" ) = workflow_call
46
+ }
47
+
48
+ InputsStmt getInputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "inputs" ) }
49
+
50
+ OutputsStmt getOutputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "outputs" ) }
51
+
52
+ string getName ( ) { result = this .getLocation ( ) .getFile ( ) .getRelativePath ( ) }
53
+ }
54
+
55
+ class InputsStmt extends Statement instanceof YamlMapping {
56
+ InputsStmt ( ) {
57
+ exists ( Actions:: On on | on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "inputs" ) = this )
58
+ }
59
+
60
+ /**
61
+ * Gets a specific parameter expression (YamlMapping) by name.
62
+ * eg:
63
+ * on:
64
+ * workflow_call:
65
+ * inputs:
66
+ * config-path:
67
+ * required: true
68
+ * type: string
69
+ * secrets:
70
+ * token:
71
+ * required: true
72
+ */
73
+ InputExpr getInputExpr ( string name ) {
74
+ result .( YamlString ) .getValue ( ) = name and
75
+ this .( YamlMapping ) .maps ( result , _)
76
+ }
37
77
}
38
78
79
+ class InputExpr extends Expression instanceof YamlString { }
80
+
81
+ class OutputsStmt extends Statement instanceof YamlMapping {
82
+ OutputsStmt ( ) {
83
+ exists ( Actions:: On on | on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "outputs" ) = this )
84
+ }
85
+
86
+ /**
87
+ * Gets a specific parameter expression (YamlMapping) by name.
88
+ * eg:
89
+ * on:
90
+ * workflow_call:
91
+ * outputs:
92
+ * firstword:
93
+ * description: "The first output string"
94
+ * value: ${{ jobs.example_job.outputs.output1 }}
95
+ * secondword:
96
+ * description: "The second output string"
97
+ * value: ${{ jobs.example_job.outputs.output2 }}
98
+ */
99
+ OutputExpr getOutputExpr ( string name ) {
100
+ this .( YamlMapping ) .lookup ( name ) .( YamlMapping ) .lookup ( "value" ) = result
101
+ }
102
+ }
103
+
104
+ class OutputExpr extends Expression instanceof YamlString { }
105
+
39
106
/**
40
107
* A Job is a collection of steps that run in an execution environment.
41
108
*/
@@ -71,6 +138,16 @@ class JobStmt extends Statement instanceof Actions::Job {
71
138
* out2: ${steps.foo.baz}
72
139
*/
73
140
JobOutputStmt getOutputStmt ( ) { result = this .( Actions:: Job ) .lookup ( "outputs" ) }
141
+
142
+ /**
143
+ * Reusable workflow jobs may have Uses children
144
+ * eg:
145
+ * call-job:
146
+ * uses: ./.github/workflows/reusable_workflow.yml
147
+ * with:
148
+ * arg1: value1
149
+ */
150
+ JobUsesExpr getUsesExpr ( ) { result .getJob ( ) = this }
74
151
}
75
152
76
153
/**
@@ -104,26 +181,85 @@ class StepStmt extends Statement instanceof Actions::Step {
104
181
JobStmt getJob ( ) { result = super .getJob ( ) }
105
182
}
106
183
184
+ /**
185
+ * Abstract class representing a call to a 3rd party action or reusable workflow.
186
+ */
187
+ abstract class UsesExpr extends Expression {
188
+ abstract string getCallee ( ) ;
189
+
190
+ abstract string getVersion ( ) ;
191
+
192
+ abstract Expression getArgument ( string key ) ;
193
+ }
194
+
107
195
/**
108
196
* A Uses step represents a call to an action that is defined in a GitHub repository.
109
197
*/
110
- class UsesExpr extends StepStmt , Expression {
198
+ class StepUsesExpr extends StepStmt , UsesExpr {
111
199
Actions:: Uses uses ;
112
200
113
- UsesExpr ( ) { uses .getStep ( ) = this }
201
+ StepUsesExpr ( ) { uses .getStep ( ) = this }
114
202
115
- string getTarget ( ) { result = uses .getGitHubRepository ( ) }
203
+ override string getCallee ( ) { result = uses .getGitHubRepository ( ) }
116
204
117
- string getVersion ( ) { result = uses .getVersion ( ) }
205
+ override string getVersion ( ) { result = uses .getVersion ( ) }
118
206
119
- Expression getArgument ( string key ) {
207
+ override Expression getArgument ( string key ) {
120
208
exists ( Actions:: With with |
121
209
with .getStep ( ) = this and
122
210
result = with .lookup ( key )
123
211
)
124
212
}
125
213
}
126
214
215
+ /**
216
+ * A Uses step represents a call to an action that is defined in a GitHub repository.
217
+ */
218
+ class JobUsesExpr extends UsesExpr instanceof YamlMapping {
219
+ JobUsesExpr ( ) {
220
+ this instanceof JobStmt and this .maps ( any ( YamlString s | s .getValue ( ) = "uses" ) , _)
221
+ }
222
+
223
+ JobStmt getJob ( ) { result = this }
224
+
225
+ /**
226
+ * Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
227
+ * local repo: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
228
+ * local repo: ./.github/workflows/workflow-2.yml
229
+ * remote repo: octo-org/another-repo/.github/workflows/workflow.yml@v1
230
+ */
231
+ private string repoUsesParser ( ) { result = "([^/]+)/([^/]+)/([^@]+)@(.+)" }
232
+
233
+ private string pathUsesParser ( ) { result = "\\./(.+)" }
234
+
235
+ override string getCallee ( ) {
236
+ exists ( YamlString name |
237
+ this .( YamlMapping ) .lookup ( "uses" ) = name and
238
+ if name .getValue ( ) .matches ( "./%" )
239
+ then result = name .getValue ( ) .regexpCapture ( this .pathUsesParser ( ) , 1 )
240
+ else
241
+ result =
242
+ name .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 1 ) + "/" +
243
+ name .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 2 ) + "/" +
244
+ name .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 3 )
245
+ )
246
+ }
247
+
248
+ /** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
249
+ override string getVersion ( ) {
250
+ exists ( YamlString name |
251
+ this .( YamlMapping ) .lookup ( "uses" ) = name and
252
+ if not name .getValue ( ) .matches ( "\\.%" )
253
+ then result = name .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 4 )
254
+ else none ( )
255
+ )
256
+ }
257
+
258
+ override Expression getArgument ( string key ) {
259
+ this .( YamlMapping ) .lookup ( "with" ) .( YamlMapping ) .lookup ( key ) = result
260
+ }
261
+ }
262
+
127
263
/**
128
264
* A Run step represents the evaluation of a provided script
129
265
*/
@@ -183,16 +319,19 @@ class StepOutputAccessExpr extends ExprAccessExpr {
183
319
/**
184
320
* A ExprAccessExpr where the expression evaluated is a job output read.
185
321
* eg: `${{ needs.job1.outputs.foo}}`
322
+ * eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
186
323
*/
187
324
class JobOutputAccessExpr extends ExprAccessExpr {
188
325
string jobId ;
189
326
string varName ;
190
327
191
328
JobOutputAccessExpr ( ) {
192
329
jobId =
193
- this .getExpression ( ) .regexpCapture ( "needs\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+" , 1 ) and
330
+ this .getExpression ( )
331
+ .regexpCapture ( "(needs|jobs)\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+" , 2 ) and
194
332
varName =
195
- this .getExpression ( ) .regexpCapture ( "needs\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)" , 1 )
333
+ this .getExpression ( )
334
+ .regexpCapture ( "(needs|jobs)\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)" , 2 )
196
335
}
197
336
198
337
string getVarName ( ) { result = varName }
@@ -201,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
201
340
exists ( JobStmt job |
202
341
job .getId ( ) = jobId and
203
342
job .getLocation ( ) .getFile ( ) = this .getLocation ( ) .getFile ( ) and
204
- job .getOutputStmt ( ) .getOutputExpr ( varName ) = result
343
+ (
344
+ // A Job can have multiple outputs, so we need to check both
345
+ // jobs.<job_id>.outputs.<output_name>
346
+ job .getOutputStmt ( ) .getOutputExpr ( varName ) = result
347
+ or
348
+ // jobs.<job_id>.uses (variables returned from the reusable workflow
349
+ job .getUsesExpr ( ) = result
350
+ )
351
+ )
352
+ }
353
+ }
354
+
355
+ /**
356
+ * A ExprAccessExpr where the expression evaluated is a reusable workflow input read.
357
+ * eg: `${{ inputs.foo}}`
358
+ */
359
+ class ReusableWorkflowInputAccessExpr extends ExprAccessExpr {
360
+ string paramName ;
361
+
362
+ ReusableWorkflowInputAccessExpr ( ) {
363
+ paramName = this .getExpression ( ) .regexpCapture ( "inputs\\.([A-Za-z0-9_-]+)" , 1 )
364
+ }
365
+
366
+ string getParamName ( ) { result = paramName }
367
+
368
+ Expression getInputExpr ( ) {
369
+ exists ( ReusableWorkflowStmt w |
370
+ w .getLocation ( ) .getFile ( ) = this .getLocation ( ) .getFile ( ) and
371
+ w .getInputs ( ) .getInputExpr ( paramName ) = result
205
372
)
206
373
}
207
374
}
0 commit comments