Skip to content

Commit f26e41d

Browse files
author
Alvaro Muñoz
authored
Merge pull request #88 from github/DFG/composite_actions
DFG/composite actions
2 parents 610dcaf + f095622 commit f26e41d

27 files changed

+476
-182
lines changed

ql/lib/codeql/actions/Helper.qll

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -252,26 +252,10 @@ predicate inPrivilegedExternallyTriggerableJob(AstNode node) {
252252
)
253253
}
254254

255-
predicate calledByPrivilegedExternallyTriggerableJob(AstNode node) {
256-
exists(ReusableWorkflow rw, ExternalJob caller, Job callee |
257-
callee = node.getEnclosingJob() and
258-
rw.getACaller() = caller and
259-
rw.getAJob() = callee and
260-
caller.isPrivilegedExternallyTriggerable()
261-
)
262-
or
263-
exists(LocalJob caller |
264-
caller = node.getEnclosingCompositeAction().getACallerJob() and
265-
caller.isPrivilegedExternallyTriggerable()
266-
)
267-
}
268-
269255
predicate inPrivilegedContext(AstNode node) {
270256
inPrivilegedCompositeAction(node)
271257
or
272258
inPrivilegedExternallyTriggerableJob(node)
273-
or
274-
calledByPrivilegedExternallyTriggerableJob(node)
275259
}
276260

277261
predicate inNonPrivilegedCompositeAction(AstNode node) {
@@ -314,3 +298,20 @@ string defaultBranchNames() {
314298
not exists(string default_branch_name | repositoryDataModel(_, default_branch_name)) and
315299
result = ["main", "master"]
316300
}
301+
302+
string getRepoRoot() {
303+
exists(Workflow w |
304+
w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
305+
result =
306+
w.getLocation()
307+
.getFile()
308+
.getRelativePath()
309+
.prefix(w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") + 1) and
310+
// exclude workflow_enum reusable workflows directory root
311+
not result.indexOf(".github/reusable_workflows/") > -1
312+
or
313+
not w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
314+
not w.getLocation().getFile().getRelativePath().indexOf(".github/reusable_workflows") > -1 and
315+
result = ""
316+
)
317+
}

ql/lib/codeql/actions/ast/internal/Ast.qll

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -308,19 +308,22 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
308308
LocalJobImpl getACallerJob() { result = this.getACallerStep().getEnclosingJob() }
309309

310310
UsesStepImpl getACallerStep() {
311-
exists(UsesStepImpl caller, string gwf_path, string path |
312-
// the workflow files may not be rooted in the parent directory of .github/workflows
313-
// extract the offset so we can remove it from the action path
314-
gwf_path =
315-
caller
316-
.getLocation()
311+
exists(DataFlow::CallNode call |
312+
call.getCalleeNode() = this and
313+
result = call.getCfgNode().getAstNode()
314+
)
315+
}
316+
317+
string getResolvedPath() {
318+
result =
319+
["", "./"] +
320+
this.getLocation()
317321
.getFile()
318322
.getRelativePath()
319-
.prefix(caller.getLocation().getFile().getRelativePath().indexOf(".github/workflows/")) and
320-
path = this.getLocation().getFile().getRelativePath().replaceAll(gwf_path, "") and
321-
caller.getCallee() = ["", "./"] + path.prefix(path.indexOf(["/action.yml", "/action.yaml"])) and
322-
result = caller
323-
)
323+
.replaceAll(getRepoRoot(), "")
324+
.replaceAll("/action.yml", "")
325+
.replaceAll("/action.yaml", "")
326+
.replaceAll(".github/reusable_workflows/", "")
324327
}
325328

326329
private predicate hasExplicitSecretAccess() {
@@ -352,6 +355,8 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
352355
)
353356
}
354357

358+
EventImpl getATriggerEvent() { result = this.getACallerJob().getATriggerEvent() }
359+
355360
/** Holds if the action is privileged and externally triggerable. */
356361
predicate isPrivilegedExternallyTriggerable() {
357362
// the action is externally triggerable
@@ -416,6 +421,14 @@ class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl {
416421

417422
override AstNodeImpl getAChildNode() { result.getNode() = n.getAChildNode*() }
418423

424+
override EventImpl getATriggerEvent() {
425+
// The trigger event for a reusable workflow is the trigger event of the caller workflow
426+
this.getACaller().getEnclosingWorkflow().getOn().getAnEvent() = result
427+
or
428+
// or the trigger event of the workflow if it has any other than workflow_call
429+
this.getOn().getAnEvent() = result and not result.getName() = "workflow_call"
430+
}
431+
419432
OutputsImpl getOutputs() { result.getNode() = workflow_call.(YamlMapping).lookup("outputs") }
420433

421434
ExpressionImpl getAnOutputExpr() { result = this.getOutputs().getAnOutputExpr() }
@@ -439,6 +452,16 @@ class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl {
439452
result = call.getCfgNode().getAstNode()
440453
)
441454
}
455+
456+
string getResolvedPath() {
457+
result =
458+
["", "./"] +
459+
this.getLocation()
460+
.getFile()
461+
.getRelativePath()
462+
.replaceAll(getRepoRoot(), "")
463+
.replaceAll(".github/reusable_workflows/", "")
464+
}
442465
}
443466

444467
class InputsImpl extends AstNodeImpl, TInputsNode {
@@ -796,12 +819,16 @@ class JobImpl extends AstNodeImpl, TJobNode {
796819
StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") }
797820

798821
/** Gets the trigger event that starts this workflow. */
799-
EventImpl getATriggerEvent() { result = this.getEnclosingWorkflow().getATriggerEvent() }
822+
EventImpl getATriggerEvent() {
823+
if this.getEnclosingWorkflow() instanceof ReusableWorkflowImpl
824+
then
825+
result = this.getEnclosingWorkflow().(ReusableWorkflowImpl).getACaller().getATriggerEvent()
826+
or
827+
result = this.getEnclosingWorkflow().getATriggerEvent() and
828+
not result.getName() = "workflow_call"
829+
else result = this.getEnclosingWorkflow().getATriggerEvent()
830+
}
800831

801-
// private predicate hasSingleTrigger(string trigger) {
802-
// this.getATriggerEvent().getName() = trigger and
803-
// count(this.getATriggerEvent()) = 1
804-
// }
805832
/** Gets the runs-on field of the job. */
806833
string getARunsOnLabel() {
807834
exists(ScalarValueImpl lbl, YamlMappingLikeNode runson |
@@ -839,9 +866,8 @@ class JobImpl extends AstNodeImpl, TJobNode {
839866
)
840867
}
841868

842-
private predicate hasExplicitWritePermission() {
843-
// the job has an explicit write permission
844-
this.getPermissions().getAPermission().matches("%write")
869+
private predicate hasExplicitNonePermission() {
870+
exists(this.getPermissions()) and not exists(this.getPermissions().getAPermission())
845871
}
846872

847873
private predicate hasExplicitReadPermission() {
@@ -850,15 +876,57 @@ class JobImpl extends AstNodeImpl, TJobNode {
850876
not this.getPermissions().getAPermission().matches("%write")
851877
}
852878

853-
private predicate hasImplicitWritePermission() {
879+
private predicate hasExplicitWritePermission() {
854880
// the job has an explicit write permission
855-
this.getEnclosingWorkflow().getPermissions().getAPermission().matches("%write")
881+
this.getPermissions().getAPermission().matches("%write")
882+
}
883+
884+
private predicate hasImplicitNonePermission() {
885+
not exists(this.getPermissions()) and
886+
exists(this.getEnclosingWorkflow().getPermissions()) and
887+
not exists(this.getEnclosingWorkflow().getPermissions().getAPermission())
888+
or
889+
not exists(this.getPermissions()) and
890+
not exists(this.getEnclosingWorkflow().getPermissions()) and
891+
exists(this.getEnclosingWorkflow().(ReusableWorkflowImpl).getACaller().getPermissions()) and
892+
not exists(
893+
this.getEnclosingWorkflow()
894+
.(ReusableWorkflowImpl)
895+
.getACaller()
896+
.getPermissions()
897+
.getAPermission()
898+
)
856899
}
857900

858901
private predicate hasImplicitReadPermission() {
859902
// the job has not an explicit write permission
903+
not exists(this.getPermissions()) and
860904
exists(this.getEnclosingWorkflow().getPermissions().getAPermission()) and
861905
not this.getEnclosingWorkflow().getPermissions().getAPermission().matches("%write")
906+
or
907+
not exists(this.getPermissions()) and
908+
not exists(this.getEnclosingWorkflow().getPermissions()) and
909+
this.getEnclosingWorkflow()
910+
.(ReusableWorkflowImpl)
911+
.getACaller()
912+
.getPermissions()
913+
.getAPermission()
914+
.matches("%read")
915+
}
916+
917+
private predicate hasImplicitWritePermission() {
918+
// the job has an explicit write permission
919+
not exists(this.getPermissions()) and
920+
this.getEnclosingWorkflow().getPermissions().getAPermission().matches("%write")
921+
or
922+
not exists(this.getPermissions()) and
923+
not exists(this.getEnclosingWorkflow().getPermissions()) and
924+
this.getEnclosingWorkflow()
925+
.(ReusableWorkflowImpl)
926+
.getACaller()
927+
.getPermissions()
928+
.getAPermission()
929+
.matches("%write")
862930
}
863931

864932
private predicate hasRuntimeData() {
@@ -917,6 +985,8 @@ class JobImpl extends AstNodeImpl, TJobNode {
917985
// and the job is not explicitly non-privileged
918986
not (
919987
(
988+
this.hasExplicitNonePermission() or
989+
this.hasImplicitNonePermission() or
920990
this.hasExplicitReadPermission() or
921991
this.hasImplicitReadPermission()
922992
) and
@@ -1174,15 +1244,6 @@ abstract class UsesImpl extends AstNodeImpl {
11741244
}
11751245
}
11761246

1177-
/**
1178-
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
1179-
* The capture groups are:
1180-
* 1: The owner of the repository where the Action comes from, e.g. `actions` in `actions/checkout@v2`
1181-
* 2: The name of the repository where the Action comes from, e.g. `checkout` in `actions/checkout@v2`.
1182-
* 3: The version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`.
1183-
*/
1184-
private string usesParser() { result = "([^/]+)/([^/@]+)@(.+)" }
1185-
11861247
/** A Uses step represents a call to an action that is defined in a GitHub repository. */
11871248
class UsesStepImpl extends StepImpl, UsesImpl {
11881249
YamlScalar u;
@@ -1194,19 +1255,14 @@ class UsesStepImpl extends StepImpl, UsesImpl {
11941255
/** Gets the owner and name of the repository where the Action comes from, e.g. `actions/checkout` in `actions/checkout@v2`. */
11951256
override string getCallee() {
11961257
if u.getValue().indexOf("@") > 0
1197-
then
1198-
result =
1199-
(
1200-
u.getValue().regexpCapture(usesParser(), 1) + "/" +
1201-
u.getValue().regexpCapture(usesParser(), 2)
1202-
).toLowerCase()
1258+
then result = u.getValue().prefix(u.getValue().indexOf("@"))
12031259
else result = u.getValue()
12041260
}
12051261

12061262
override ScalarValueImpl getCalleeNode() { result.getNode() = u }
12071263

12081264
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
1209-
override string getVersion() { result = u.getValue().regexpCapture(usesParser(), 3) }
1265+
override string getVersion() { result = u.getValue().suffix(u.getValue().indexOf("@") + 1) }
12101266

12111267
override string toString() {
12121268
if exists(this.getId()) then result = "Uses Step: " + this.getId() else result = "Uses Step"

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ private class CompositeActionTree extends StandardPreOrderTree instanceof Compos
148148
rank[i](AstNode child, Location l |
149149
(
150150
child = this.(CompositeAction).getAnInput() or
151-
child = this.(CompositeAction).getAnOutputExpr() or
151+
child = this.(CompositeAction).getOutputs() or
152152
child = this.(CompositeAction).getRuns()
153153
) and
154154
l = child.getLocation()
@@ -172,7 +172,7 @@ private class WorkflowTree extends StandardPreOrderTree instanceof Workflow {
172172
rank[i](AstNode child, Location l |
173173
(
174174
child = this.(ReusableWorkflow).getAnInput() or
175-
child = this.(ReusableWorkflow).getAnOutputExpr() or
175+
child = this.(ReusableWorkflow).getOutputs() or
176176
child = this.(ReusableWorkflow).getStrategy() or
177177
child = this.(ReusableWorkflow).getAJob()
178178
) and
@@ -202,7 +202,7 @@ private class OutputsTree extends StandardPreOrderTree instanceof Outputs {
202202
override ControlFlowTree getChildNode(int i) {
203203
result =
204204
rank[i](AstNode child, Location l |
205-
child = super.getOutputExpr(_) and l = child.getLocation()
205+
child = super.getAnOutputExpr() and l = child.getLocation()
206206
|
207207
child
208208
order by

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

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class DataFlowExpr extends Cfg::Node {
7070
}
7171

7272
/**
73-
* A call corresponds to a Uses steps where a local action, 3rd party action or a reusable workflow get called
73+
* A call corresponds to a Uses steps where a composite action or a reusable workflow get called
7474
*/
7575
class DataFlowCall instanceof Cfg::Node {
7676
DataFlowCall() { super.getAstNode() instanceof Uses }
@@ -89,46 +89,15 @@ class DataFlowCall instanceof Cfg::Node {
8989
Location getLocation() { result = this.(Cfg::Node).getLocation() }
9090
}
9191

92-
string getRepoRoot() {
93-
exists(Workflow w |
94-
w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
95-
result =
96-
w.getLocation()
97-
.getFile()
98-
.getRelativePath()
99-
.prefix(w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") + 1) and
100-
// exclude workflow_enum reusable workflows directory root
101-
not result.indexOf(".github/reusable_workflows/") > -1
102-
or
103-
not w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
104-
not w.getLocation().getFile().getRelativePath().indexOf(".github/reusable_workflows") > -1 and
105-
result = ""
106-
)
107-
}
108-
10992
/**
11093
* A Cfg scope that can be called
11194
*/
11295
class DataFlowCallable instanceof Cfg::CfgScope {
11396
string toString() { result = super.toString() }
11497

11598
string getName() {
116-
if this instanceof ReusableWorkflow
117-
then result = this.(ReusableWorkflow).getLocation().getFile().getRelativePath() // or
118-
else
119-
if this instanceof CompositeAction
120-
then
121-
result =
122-
this.(CompositeAction)
123-
.getLocation()
124-
.getFile()
125-
.getRelativePath()
126-
.prefix(this.(CompositeAction)
127-
.getLocation()
128-
.getFile()
129-
.getRelativePath()
130-
.indexOf(["/action.yml", "/action.yaml"]))
131-
else none()
99+
result = this.(ReusableWorkflowImpl).getResolvedPath() or
100+
result = this.(CompositeActionImpl).getResolvedPath()
132101
}
133102

134103
/** Gets a best-effort total ordering. */
@@ -150,13 +119,7 @@ class NormalReturn extends ReturnKind, TNormalReturn {
150119
}
151120

152121
/** Gets a viable implementation of the target of the given `Call`. */
153-
DataFlowCallable viableCallable(DataFlowCall c) {
154-
c.getName() = result.getName() or
155-
c.getName() = result.getName().replaceAll(getRepoRoot(), "") or
156-
// special case for reusable workflows downloaded by the workflow_enum action
157-
c.getName() =
158-
result.getName().replaceAll(getRepoRoot(), "").replaceAll(".github/reusable_workflows/", "")
159-
}
122+
DataFlowCallable viableCallable(DataFlowCall c) { c.getName() = result.getName() }
160123

161124
/**
162125
* Gets a node that can read the value returned from `call` with return kind

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ class ReturnNode extends ExprNode {
9696

9797
ReturnNode() {
9898
this.asExpr() = outputs and
99-
outputs = any(ReusableWorkflow s).getOutputs()
99+
(
100+
exists(ReusableWorkflow w | w.getOutputs() = outputs) or
101+
exists(CompositeAction a | a.getOutputs() = outputs)
102+
)
100103
}
101104

102105
ReturnKind getKind() { result = TNormalReturn() }

ql/lib/codeql/actions/security/ControlChecks.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ abstract class ControlCheck extends AstNode {
3838
}
3939

4040
predicate protects(Step step, Event event, string category) {
41-
event.getEnclosingWorkflow() = step.getEnclosingWorkflow() and
41+
event = step.getEnclosingWorkflow().getATriggerEvent() and
4242
this.dominates(step) and
4343
this.protectsCategoryAndEvent(category, event.getName())
4444
}

0 commit comments

Comments
 (0)