Skip to content

Commit 9659098

Browse files
author
Alvaro Muñoz
committed
Support for Reusable workflows
1 parent db41336 commit 9659098

File tree

5 files changed

+217
-50
lines changed

5 files changed

+217
-50
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,51 @@ 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 }
3739
}
3840

41+
class ReusableWorkflowStmt extends WorkflowStmt {
42+
YamlMapping parameters;
43+
44+
ReusableWorkflowStmt() {
45+
exists(Actions::On on |
46+
on.getWorkflow() = this and
47+
on.getNode("workflow_call").(YamlMapping).lookup("inputs") = parameters
48+
)
49+
}
50+
51+
ParamsStmt getParams() { result = parameters }
52+
53+
// TODO: implemnt callable name
54+
string getName() { result = this.getLocation().getFile().getRelativePath() }
55+
}
56+
57+
class ParamsStmt extends Statement instanceof YamlMapping {
58+
ParamsStmt() {
59+
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("inputs") = this)
60+
}
61+
62+
/**
63+
* Gets a specific parameter expression (YamlMapping) by name.
64+
* eg:
65+
* on:
66+
* workflow_call:
67+
* inputs:
68+
* config-path:
69+
* required: true
70+
* type: string
71+
* secrets:
72+
* token:
73+
* required: true
74+
*/
75+
ParamExpr getParamExpr(string name) {
76+
this.(YamlMapping).maps(any(YamlScalar s | s.getValue() = name), result)
77+
}
78+
}
79+
80+
class ParamExpr extends Expression instanceof YamlValue { }
81+
3982
/**
4083
* A Job is a collection of steps that run in an execution environment.
4184
*/
@@ -71,6 +114,11 @@ class JobStmt extends Statement instanceof Actions::Job {
71114
* out2: ${steps.foo.baz}
72115
*/
73116
JobOutputStmt getOutputStmt() { result = this.(Actions::Job).lookup("outputs") }
117+
118+
/**
119+
* Reusable workflow jobs may have Uses children
120+
*/
121+
JobUsesExpr getUsesExpr() { result = this.(Actions::Job).lookup("uses") }
74122
}
75123

76124
/**
@@ -104,26 +152,82 @@ class StepStmt extends Statement instanceof Actions::Step {
104152
JobStmt getJob() { result = super.getJob() }
105153
}
106154

155+
abstract class UsesExpr extends Expression {
156+
abstract string getTarget();
157+
158+
abstract string getVersion();
159+
160+
abstract Expression getArgument(string key);
161+
}
162+
107163
/**
108164
* A Uses step represents a call to an action that is defined in a GitHub repository.
109165
*/
110-
class UsesExpr extends StepStmt, Expression {
166+
class StepUsesExpr extends StepStmt, UsesExpr {
111167
Actions::Uses uses;
112168

113-
UsesExpr() { uses.getStep() = this }
169+
StepUsesExpr() { uses.getStep() = this }
114170

115-
string getTarget() { result = uses.getGitHubRepository() }
171+
override string getTarget() { result = uses.getGitHubRepository() }
116172

117-
string getVersion() { result = uses.getVersion() }
173+
override string getVersion() { result = uses.getVersion() }
118174

119-
Expression getArgument(string key) {
175+
override Expression getArgument(string key) {
120176
exists(Actions::With with |
121177
with.getStep() = this and
122178
result = with.lookup(key)
123179
)
124180
}
125181
}
126182

183+
/**
184+
* A Uses step represents a call to an action that is defined in a GitHub repository.
185+
*/
186+
class JobUsesExpr extends UsesExpr instanceof YamlScalar {
187+
JobStmt job;
188+
189+
JobUsesExpr() { job.(YamlMapping).lookup("uses") = this }
190+
191+
JobStmt getJob() { result = job }
192+
193+
/**
194+
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
195+
* local repo: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
196+
* local repo: ./.github/workflows/workflow-2.yml
197+
* remote repo: octo-org/another-repo/.github/workflows/workflow.yml@v1
198+
*/
199+
private string repoUsesParser() { result = "([^/]+)/([^/]+)/([^@]+)@(.+)" }
200+
201+
private string pathUsesParser() { result = "\\./(.+)" }
202+
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)
208+
else
209+
result =
210+
name.regexpCapture(this.repoUsesParser(), 1) + "/" +
211+
name.regexpCapture(this.repoUsesParser(), 2) + "/" +
212+
name.regexpCapture(this.repoUsesParser(), 3)
213+
)
214+
}
215+
216+
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
217+
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)
222+
else none()
223+
)
224+
}
225+
226+
override Expression getArgument(string key) {
227+
job.(YamlMapping).lookup("with").(YamlMapping).lookup(key) = result
228+
}
229+
}
230+
127231
/**
128232
* A Run step represents the evaluation of a provided script
129233
*/

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

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

90+
private class ReusableWorkflowScope extends CfgScope instanceof ReusableWorkflowStmt { }
91+
9092
private class JobScope extends CfgScope instanceof JobStmt { }
9193
}
9294

@@ -120,9 +122,15 @@ private module Implementation implements CfgShared::InputSig<Location> {
120122

121123
int maxSplits() { result = 0 }
122124

123-
predicate scopeFirst(CfgScope scope, AstNode e) { first(scope.(JobStmt), e) }
125+
predicate scopeFirst(CfgScope scope, AstNode e) {
126+
first(scope.(ReusableWorkflowStmt).getParams(), e) or
127+
first(scope.(JobStmt), e)
128+
}
124129

125-
predicate scopeLast(CfgScope scope, AstNode e, Completion c) { last(scope.(JobStmt), e, c) }
130+
predicate scopeLast(CfgScope scope, AstNode e, Completion c) {
131+
last(scope.(ReusableWorkflowStmt), e, c) or
132+
last(scope.(JobStmt), e, c)
133+
}
126134

127135
predicate successorTypeIsSimple(SuccessorType t) { t instanceof NormalSuccessor }
128136

@@ -139,11 +147,30 @@ private import CfgImpl
139147
private import Completion
140148
private import CfgScope
141149

150+
private class ReusableWorkflowTree extends StandardPreOrderTree instanceof ReusableWorkflowStmt {
151+
override ControlFlowTree getChildNode(int i) { result = super.getParams() and i = 0 }
152+
}
153+
154+
private class ReusableWorkflowParamsTree extends StandardPreOrderTree instanceof ParamsStmt {
155+
override ControlFlowTree getChildNode(int i) {
156+
result =
157+
rank[i](Expression child, Location l |
158+
child = super.getParamExpr(_) and l = child.getLocation()
159+
|
160+
child
161+
order by
162+
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
163+
)
164+
}
165+
}
166+
167+
private class ParamExprTree extends LeafTree instanceof ParamExpr { }
168+
142169
private class JobTree extends StandardPreOrderTree instanceof JobStmt {
143170
override ControlFlowTree getChildNode(int i) {
144171
result =
145172
rank[i](Expression child, Location l |
146-
(child = super.getAStep() or child = super.getOutputStmt()) and
173+
(child = super.getAStep() or child = super.getOutputStmt() or child = super.getUsesExpr()) and
147174
l = child.getLocation()
148175
|
149176
child
@@ -157,7 +184,20 @@ private class JobOutputTree extends StandardPreOrderTree instanceof JobOutputStm
157184
override ControlFlowTree getChildNode(int i) { result = super.asYamlMapping().getValueNode(i) }
158185
}
159186

160-
private class UsesTree extends StandardPreOrderTree instanceof UsesExpr {
187+
private class StepUsesTree extends StandardPreOrderTree instanceof StepUsesExpr {
188+
override ControlFlowTree getChildNode(int i) {
189+
result =
190+
rank[i](Expression child, Location l |
191+
child = super.getArgument(_) and l = child.getLocation()
192+
|
193+
child
194+
order by
195+
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
196+
)
197+
}
198+
}
199+
200+
private class JobUsesTree extends StandardPreOrderTree instanceof JobUsesExpr {
161201
override ControlFlowTree getChildNode(int i) {
162202
result =
163203
rank[i](Expression child, Location l |

ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,32 @@ private import codeql.actions.controlflow.BasicBlocks
66
private import DataFlowPublic
77

88
cached
9-
newtype TNode = TExprNode(DataFlowExpr e)
9+
newtype TNode =
10+
TExprNode(DataFlowExpr e) or
11+
TParameterNode(ParamExpr p) { p = any(ReusableWorkflowStmt w).getParams().getParamExpr(_) } or
12+
TReturningNode(Cfg::Node n) { n.getAstNode() = any(JobStmt j).getOutputStmt().getOutputExpr(_) }
1013

1114
/**
12-
* Not used
15+
* Reusable workflow input nodes
1316
*/
14-
class ParameterNode extends Node {
15-
ParameterNode() { none() }
17+
class ParameterNode extends Node, TParameterNode {
18+
private ParamExpr parameter;
19+
20+
ParameterNode() { this = TParameterNode(parameter) }
21+
22+
predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
23+
parameter = c.(ReusableWorkflowStmt).getParams().getParamExpr(pos)
24+
}
25+
26+
override string toString() { result = parameter.toString() }
27+
28+
override Location getLocation() { result = parameter.getLocation() }
29+
30+
ParamExpr getParameter() { result = parameter }
1631
}
1732

1833
/**
19-
* Not used
34+
* Reusable workflow output nodes
2035
*/
2136
class ReturnNode extends Node {
2237
ReturnNode() { none() }
@@ -35,17 +50,25 @@ class OutNode extends ExprNode {
3550
}
3651
}
3752

53+
/**
54+
* Not used
55+
*/
3856
class CastNode extends Node {
3957
CastNode() { none() }
4058
}
4159

60+
/**
61+
* Not used
62+
*/
4263
class PostUpdateNode extends Node {
4364
PostUpdateNode() { none() }
4465

4566
Node getPreUpdateNode() { none() }
4667
}
4768

48-
predicate isParameterNode(ParameterNode p, DataFlowCallable c, ParameterPosition pos) { none() }
69+
predicate isParameterNode(ParameterNode p, DataFlowCallable c, ParameterPosition pos) {
70+
p.isParameterOf(c, pos)
71+
}
4972

5073
predicate isArgumentNode(ArgumentNode arg, DataFlowCall call, ArgumentPosition pos) {
5174
arg.argumentOf(call, pos)
@@ -64,7 +87,7 @@ class DataFlowExpr extends Cfg::Node {
6487
}
6588

6689
/**
67-
* A call corresponds to a Uses steps where a 3rd party action gets called
90+
* A call corresponds to a Uses steps where a 3rd party action or a reusable workflow gets called
6891
*/
6992
class DataFlowCall instanceof Cfg::Node {
7093
DataFlowCall() { super.getAstNode() instanceof UsesExpr }
@@ -79,27 +102,16 @@ class DataFlowCall instanceof Cfg::Node {
79102
DataFlowCallable getEnclosingCallable() { result = super.getScope() }
80103
}
81104

82-
// class DataFlowCallable instanceof Cfg::CfgScope {
83-
// DataFlowCallable() { none() }
84-
//
85-
// string toString() { result = super.toString() }
86-
//
87-
// string getName() { result = "none" }
88-
// }
89105
/**
90106
* A Cfg scope that can be called
91-
* There are no callables in Actions, at least not in the AST
107+
* ReusableWorkflowStmt
92108
*/
93-
class DataFlowCallable instanceof Cfg::CfgScope {
109+
class DataFlowCallable instanceof ReusableWorkflowStmt {
94110
string toString() { result = super.toString() }
95111

96112
Location getLocation() { result = super.getLocation() }
97113

98-
string getName() {
99-
if this instanceof StepStmt
100-
then result = this.(StepStmt).getId()
101-
else result = this.(JobStmt).getId()
102-
}
114+
string getName() { result = super.getName() }
103115
}
104116

105117
newtype TReturnKind = TNormalReturn()
@@ -114,7 +126,7 @@ class NormalReturn extends ReturnKind, TNormalReturn {
114126
}
115127

116128
/** Gets a viable implementation of the target of the given `Call`. */
117-
DataFlowCallable viableCallable(DataFlowCall c) { none() }
129+
DataFlowCallable viableCallable(DataFlowCall c) { c.getName() = result.getName() }
118130

119131
// /**
120132
// * Holds if the set of viable implementations that can be called by `call`
@@ -173,11 +185,10 @@ class ContentApprox extends TContentApprox {
173185
ContentApprox getContentApprox(Content c) { none() }
174186

175187
/**
176-
* Not used since we dont have Callables in the AST
177188
* Made a string to match the ArgumentPosition type
178189
*/
179190
class ParameterPosition extends string {
180-
ParameterPosition() { none() }
191+
ParameterPosition() { exists(any(ReusableWorkflowStmt w).getParams().getParamExpr(this)) }
181192
}
182193

183194
/**
@@ -188,18 +199,12 @@ class ArgumentPosition extends string {
188199
}
189200

190201
/**
191-
* Not really used since we dont have Callables in the AST but needed for the InputSig signature
192202
*/
193203
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }
194204

195-
/**
196-
* a simple local flow step
197-
*/
198-
predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFrom, nodeTo) }
199-
200-
predicate usesOutputDefToUse(Node nodeFrom, Node nodeTo) {
205+
predicate stepUsesOutputDefToUse(Node nodeFrom, Node nodeTo) {
201206
// nodeTo is an OutputVarAccessExpr scoped with the namespace of the nodeFrom Step output
202-
exists(UsesExpr uses, StepOutputAccessExpr outputRead |
207+
exists(StepUsesExpr uses, StepOutputAccessExpr outputRead |
203208
uses = nodeFrom.asExpr() and
204209
outputRead = nodeTo.asExpr() and
205210
outputRead.getStepId() = uses.getId() and
@@ -233,11 +238,16 @@ predicate jobOutputDefToUse(Node nodeFrom, Node nodeTo) {
233238
*/
234239
pragma[nomagic]
235240
predicate localFlowStep(Node nodeFrom, Node nodeTo) {
236-
usesOutputDefToUse(nodeFrom, nodeTo) or
241+
stepUsesOutputDefToUse(nodeFrom, nodeTo) or
237242
runOutputDefToUse(nodeFrom, nodeTo) or
238243
jobOutputDefToUse(nodeFrom, nodeTo)
239244
}
240245

246+
/**
247+
* a simple local flow step
248+
*/
249+
predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFrom, nodeTo) }
250+
241251
/**
242252
* Holds if data can flow from `node1` to `node2` through a non-local step
243253
* that does not follow a call edge. For example, a step through a global

0 commit comments

Comments
 (0)