@@ -34,8 +34,75 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
3434 JobStmt getAJob ( ) { result = super .getJob ( _) }
3535
3636 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+ }
3777}
3878
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+
39106/**
40107 * A Job is a collection of steps that run in an execution environment.
41108 */
@@ -71,6 +138,16 @@ class JobStmt extends Statement instanceof Actions::Job {
71138 * out2: ${steps.foo.baz}
72139 */
73140 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 }
74151}
75152
76153/**
@@ -104,26 +181,85 @@ class StepStmt extends Statement instanceof Actions::Step {
104181 JobStmt getJob ( ) { result = super .getJob ( ) }
105182}
106183
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+
107195/**
108196 * A Uses step represents a call to an action that is defined in a GitHub repository.
109197 */
110- class UsesExpr extends StepStmt , Expression {
198+ class StepUsesExpr extends StepStmt , UsesExpr {
111199 Actions:: Uses uses ;
112200
113- UsesExpr ( ) { uses .getStep ( ) = this }
201+ StepUsesExpr ( ) { uses .getStep ( ) = this }
114202
115- string getTarget ( ) { result = uses .getGitHubRepository ( ) }
203+ override string getCallee ( ) { result = uses .getGitHubRepository ( ) }
116204
117- string getVersion ( ) { result = uses .getVersion ( ) }
205+ override string getVersion ( ) { result = uses .getVersion ( ) }
118206
119- Expression getArgument ( string key ) {
207+ override Expression getArgument ( string key ) {
120208 exists ( Actions:: With with |
121209 with .getStep ( ) = this and
122210 result = with .lookup ( key )
123211 )
124212 }
125213}
126214
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+
127263/**
128264 * A Run step represents the evaluation of a provided script
129265 */
@@ -183,16 +319,19 @@ class StepOutputAccessExpr extends ExprAccessExpr {
183319/**
184320 * A ExprAccessExpr where the expression evaluated is a job output read.
185321 * eg: `${{ needs.job1.outputs.foo}}`
322+ * eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
186323 */
187324class JobOutputAccessExpr extends ExprAccessExpr {
188325 string jobId ;
189326 string varName ;
190327
191328 JobOutputAccessExpr ( ) {
192329 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
194332 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 )
196335 }
197336
198337 string getVarName ( ) { result = varName }
@@ -201,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
201340 exists ( JobStmt job |
202341 job .getId ( ) = jobId and
203342 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
205372 )
206373 }
207374}
0 commit comments