Skip to content

Commit 3152ed7

Browse files
author
Alvaro Muñoz
committed
dataflow through reusable workflows
1 parent 9659098 commit 3152ed7

File tree

13 files changed

+298
-98
lines changed

13 files changed

+298
-98
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,21 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
3939
}
4040

4141
class ReusableWorkflowStmt extends WorkflowStmt {
42-
YamlMapping parameters;
42+
YamlValue workflow_call;
4343

4444
ReusableWorkflowStmt() {
45-
exists(Actions::On on |
46-
on.getWorkflow() = this and
47-
on.getNode("workflow_call").(YamlMapping).lookup("inputs") = parameters
48-
)
45+
this.(Actions::Workflow).getOn().getNode("workflow_call") = workflow_call
4946
}
5047

51-
ParamsStmt getParams() { result = parameters }
48+
InputsStmt getInputs() { result = workflow_call.(YamlMapping).lookup("inputs") }
49+
50+
OutputsStmt getOutputs() { result = workflow_call.(YamlMapping).lookup("outputs") }
5251

53-
// TODO: implemnt callable name
5452
string getName() { result = this.getLocation().getFile().getRelativePath() }
5553
}
5654

57-
class ParamsStmt extends Statement instanceof YamlMapping {
58-
ParamsStmt() {
55+
class InputsStmt extends Statement instanceof YamlMapping {
56+
InputsStmt() {
5957
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("inputs") = this)
6058
}
6159

@@ -72,12 +70,38 @@ class ParamsStmt extends Statement instanceof YamlMapping {
7270
* token:
7371
* required: true
7472
*/
75-
ParamExpr getParamExpr(string name) {
76-
this.(YamlMapping).maps(any(YamlScalar s | s.getValue() = name), result)
73+
InputExpr getInputExpr(string name) {
74+
result.(YamlString).getValue() = name and
75+
this.(YamlMapping).maps(result, _)
7776
}
7877
}
7978

80-
class ParamExpr extends Expression instanceof YamlValue { }
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 { }
81105

82106
/**
83107
* A Job is a collection of steps that run in an execution environment.
@@ -117,8 +141,13 @@ class JobStmt extends Statement instanceof Actions::Job {
117141

118142
/**
119143
* Reusable workflow jobs may have Uses children
144+
* eg:
145+
* call-job:
146+
* uses: ./.github/workflows/reusable_workflow.yml
147+
* with:
148+
* arg1: value1
120149
*/
121-
JobUsesExpr getUsesExpr() { result = this.(Actions::Job).lookup("uses") }
150+
JobUsesExpr getUsesExpr() { result.getJob() = this }
122151
}
123152

124153
/**
@@ -152,8 +181,11 @@ class StepStmt extends Statement instanceof Actions::Step {
152181
JobStmt getJob() { result = super.getJob() }
153182
}
154183

184+
/**
185+
* Abstract class representing a call to a 3rd party action or reusable workflow.
186+
*/
155187
abstract class UsesExpr extends Expression {
156-
abstract string getTarget();
188+
abstract string getCallee();
157189

158190
abstract string getVersion();
159191

@@ -168,7 +200,7 @@ class StepUsesExpr extends StepStmt, UsesExpr {
168200

169201
StepUsesExpr() { uses.getStep() = this }
170202

171-
override string getTarget() { result = uses.getGitHubRepository() }
203+
override string getCallee() { result = uses.getGitHubRepository() }
172204

173205
override string getVersion() { result = uses.getVersion() }
174206

@@ -183,12 +215,12 @@ class StepUsesExpr extends StepStmt, UsesExpr {
183215
/**
184216
* A Uses step represents a call to an action that is defined in a GitHub repository.
185217
*/
186-
class JobUsesExpr extends UsesExpr instanceof YamlScalar {
187-
JobStmt job;
188-
189-
JobUsesExpr() { job.(YamlMapping).lookup("uses") = this }
218+
class JobUsesExpr extends UsesExpr instanceof YamlMapping {
219+
JobUsesExpr() {
220+
this instanceof JobStmt and this.maps(any(YamlString s | s.getValue() = "uses"), _)
221+
}
190222

191-
JobStmt getJob() { result = job }
223+
JobStmt getJob() { result = this }
192224

193225
/**
194226
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
@@ -200,31 +232,31 @@ class JobUsesExpr extends UsesExpr instanceof YamlScalar {
200232

201233
private string pathUsesParser() { result = "\\./(.+)" }
202234

203-
override string getTarget() {
204-
exists(string name |
205-
this.(YamlScalar).getValue() = name and
206-
if name.matches("./%")
207-
then result = name.regexpCapture(this.pathUsesParser(), 1)
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)
208240
else
209241
result =
210-
name.regexpCapture(this.repoUsesParser(), 1) + "/" +
211-
name.regexpCapture(this.repoUsesParser(), 2) + "/" +
212-
name.regexpCapture(this.repoUsesParser(), 3)
242+
name.getValue().regexpCapture(this.repoUsesParser(), 1) + "/" +
243+
name.getValue().regexpCapture(this.repoUsesParser(), 2) + "/" +
244+
name.getValue().regexpCapture(this.repoUsesParser(), 3)
213245
)
214246
}
215247

216248
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
217249
override string getVersion() {
218-
exists(string name |
219-
this.(YamlScalar).getValue() = name and
220-
if not name.matches("\\.%")
221-
then result = this.(YamlScalar).getValue().regexpCapture(this.repoUsesParser(), 4)
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)
222254
else none()
223255
)
224256
}
225257

226258
override Expression getArgument(string key) {
227-
job.(YamlMapping).lookup("with").(YamlMapping).lookup(key) = result
259+
this.(YamlMapping).lookup("with").(YamlMapping).lookup(key) = result
228260
}
229261
}
230262

@@ -287,16 +319,19 @@ class StepOutputAccessExpr extends ExprAccessExpr {
287319
/**
288320
* A ExprAccessExpr where the expression evaluated is a job output read.
289321
* eg: `${{ needs.job1.outputs.foo}}`
322+
* eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
290323
*/
291324
class JobOutputAccessExpr extends ExprAccessExpr {
292325
string jobId;
293326
string varName;
294327

295328
JobOutputAccessExpr() {
296329
jobId =
297-
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
298332
varName =
299-
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)
300335
}
301336

302337
string getVarName() { result = varName }
@@ -305,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
305340
exists(JobStmt job |
306341
job.getId() = jobId and
307342
job.getLocation().getFile() = this.getLocation().getFile() and
308-
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
309372
)
310373
}
311374
}

ql/lib/codeql/actions/Consistency.ql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import DataFlow::DataFlow::Consistency
2+
3+

ql/lib/codeql/actions/DataFlow.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@ module DataFlow {
77
private import codeql.actions.dataflow.internal.DataFlowImplSpecific
88
import DataFlowMake<ActionsDataFlow>
99
import codeql.actions.dataflow.internal.DataFlowPublic
10+
11+
/** debug */
12+
private import codeql.actions.dataflow.internal.TaintTrackingImplSpecific
13+
import codeql.dataflow.internal.DataFlowImplConsistency as DFIC
14+
module ActionsConsistency implements DFIC::InputSig<ActionsDataFlow> { }
15+
module Consistency {
16+
import DFIC::MakeConsistency<ActionsDataFlow, ActionsTaintTracking, ActionsConsistency>
17+
}
1018
}

ql/lib/codeql/actions/controlflow/internal/Cfg.qll

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ module Completion {
8787
module CfgScope {
8888
abstract class CfgScope extends AstNode { }
8989

90-
private class ReusableWorkflowScope extends CfgScope instanceof ReusableWorkflowStmt { }
90+
class ReusableWorkflowScope extends CfgScope instanceof ReusableWorkflowStmt { }
9191

92-
private class JobScope extends CfgScope instanceof JobStmt { }
92+
class JobScope extends CfgScope instanceof JobStmt { }
9393
}
9494

9595
private module Implementation implements CfgShared::InputSig<Location> {
@@ -123,7 +123,7 @@ private module Implementation implements CfgShared::InputSig<Location> {
123123
int maxSplits() { result = 0 }
124124

125125
predicate scopeFirst(CfgScope scope, AstNode e) {
126-
first(scope.(ReusableWorkflowStmt).getParams(), e) or
126+
first(scope.(ReusableWorkflowStmt).getInputs(), e) or
127127
first(scope.(JobStmt), e)
128128
}
129129

@@ -148,14 +148,39 @@ private import Completion
148148
private import CfgScope
149149

150150
private class ReusableWorkflowTree extends StandardPreOrderTree instanceof ReusableWorkflowStmt {
151-
override ControlFlowTree getChildNode(int i) { result = super.getParams() and i = 0 }
151+
override ControlFlowTree getChildNode(int i) {
152+
result =
153+
rank[i](Expression child, Location l |
154+
(child = super.getInputs() or child = super.getOutputs()) and
155+
l = child.getLocation()
156+
|
157+
child
158+
order by
159+
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
160+
)
161+
}
162+
}
163+
164+
private class ReusableWorkflowInputsTree extends StandardPreOrderTree instanceof InputsStmt {
165+
override ControlFlowTree getChildNode(int i) {
166+
result =
167+
rank[i](Expression child, Location l |
168+
child = super.getInputExpr(_) and l = child.getLocation()
169+
|
170+
child
171+
order by
172+
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
173+
)
174+
}
152175
}
153176

154-
private class ReusableWorkflowParamsTree extends StandardPreOrderTree instanceof ParamsStmt {
177+
private class InputExprTree extends LeafTree instanceof InputExpr { }
178+
179+
private class ReusableWorkflowOutputsTree extends StandardPreOrderTree instanceof OutputsStmt {
155180
override ControlFlowTree getChildNode(int i) {
156181
result =
157182
rank[i](Expression child, Location l |
158-
child = super.getParamExpr(_) and l = child.getLocation()
183+
child = super.getOutputExpr(_) and l = child.getLocation()
159184
|
160185
child
161186
order by
@@ -164,7 +189,7 @@ private class ReusableWorkflowParamsTree extends StandardPreOrderTree instanceof
164189
}
165190
}
166191

167-
private class ParamExprTree extends LeafTree instanceof ParamExpr { }
192+
private class OutputExprTree extends LeafTree instanceof OutputExpr { }
168193

169194
private class JobTree extends StandardPreOrderTree instanceof JobStmt {
170195
override ControlFlowTree getChildNode(int i) {

ql/lib/codeql/actions/dataflow/FlowSources.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private class EventSource extends RemoteFlowSource {
127127
private class ChangedFilesSource extends RemoteFlowSource {
128128
ChangedFilesSource() {
129129
exists(UsesExpr uses |
130-
uses.getTarget() = "tj-actions/changed-files" and
130+
uses.getCallee() = "tj-actions/changed-files" and
131131
uses.getVersion() = ["v10", "v20", "v30", "v40"] and
132132
uses = this.asExpr()
133133
)

ql/lib/codeql/actions/dataflow/FlowSteps.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AdditionalTaintStep extends Unit {
2626
private class ActionsFindAndReplaceStringStep extends AdditionalTaintStep {
2727
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
2828
exists(UsesExpr u |
29-
u.getTarget() = "mad9000/actions-find-and-replace-string" and
29+
u.getCallee() = "mad9000/actions-find-and-replace-string" and
3030
pred.asExpr() = u.getArgument(["source", "replace"]) and
3131
succ.asExpr() = u
3232
)

0 commit comments

Comments
 (0)