Skip to content

Commit 4b03282

Browse files
author
Alvaro Muñoz
authored
Merge pull request #1 from github/extensionpack
Support external workflow extpacks
2 parents ef9583a + 17933cb commit 4b03282

23 files changed

+223
-68
lines changed

.github/action/src/codeql.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ export async function codeqlDatabaseAnalyze(
147147
codeql_output,
148148
];
149149

150+
const extPackPath = process.env["EXTPACK_PATH"];
151+
const extPackName = process.env["EXTPACK_NAME"];
152+
if (extPackPath !== undefined && extPackName !== undefined) {
153+
cmd.push("--additional-packs", extPackPath);
154+
cmd.push("--extension-packs", extPackName);
155+
}
156+
150157
// remote pack or local pack
151158
if (codeql.pack.startsWith("githubsecuritylab/")) {
152159
var suite = codeql.pack + ":" + codeql.suite;

action.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,49 @@ inputs:
1818
description: "CodeQL Suite to run"
1919
default: "actions-code-scanning"
2020

21+
workflow-models:
22+
description: "Workflow models"
23+
required: false
24+
2125
runs:
2226
using: 'composite'
2327
steps:
24-
- name: Do something with context
28+
- name: Process workflow models
29+
shell: bash
30+
if: inputs.workflow-models
31+
run: |
32+
// Create QLPack directory
33+
mkdir workflow-extpack
34+
cd workflow-extpack
35+
36+
// Store the extension pack file
37+
cat > models.yml << 'EOF'
38+
${{ inputs.workflow-models }}
39+
EOF
40+
41+
// Create QLPack
42+
cat > qlpack.yml << 'EOF'
43+
name: local/workflow-models
44+
library: true
45+
extensionTargets:
46+
githubsecuritylab/actions-all: '*'
47+
dataExtensions:
48+
- models.yml
49+
EOF
50+
51+
// Set env vars
52+
echo "EXTPACK_PATH=./workflow-extpack" >> $GITHUB_ENV
53+
echo "EXTPACK_NAME=local/workflow-models" >> $GITHUB_ENV
54+
55+
- name: Scan workflows
2556
shell: bash
2657
env:
2758
GITHUB_TOKEN: ${{ inputs.token }}
2859
GH_TOKEN: ${{ inputs.token }}
2960
INPUT_SOURCE-ROOT: ${{ inputs.source-root }}
3061
INPUT_SARIF-OUTPUT: ${{ inputs.sarif-output }}
3162
INPUT_SUITE: ${{ inputs.suite }}
63+
EXTPACK_PATH: ${{ inputs.extpack-path }}
64+
EXTPACK_NAME: ${{ inputs.extpack-name }}
3265
run: |
3366
node ${{ github.action_path }}/.github/action/dist/index.js

ql/lib/codeql/actions/Ast.qll

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -228,36 +228,7 @@ class Workflow extends AstNode instanceof WorkflowImpl {
228228

229229
Strategy getStrategy() { result = super.getStrategy() }
230230

231-
predicate hasSingleTrigger(string trigger) {
232-
this.getATriggerEvent() = trigger and
233-
count(this.getATriggerEvent()) = 1
234-
}
235-
236-
predicate isPrivileged() {
237-
// The Workflow has a permission to write to some scope
238-
this.getPermissions().getAPermission() = "write"
239-
or
240-
// The Workflow accesses a secret
241-
exists(SecretsExpression expr |
242-
expr.getEnclosingWorkflow() = this and not expr.getFieldName() = "GITHUB_TOKEN"
243-
)
244-
or
245-
// The Workflow is triggered by an event other than `pull_request`
246-
count(this.getATriggerEvent()) = 1 and
247-
not this.getATriggerEvent() = ["pull_request", "workflow_call"]
248-
or
249-
// The Workflow is only triggered by `workflow_call` and there is
250-
// a caller workflow triggered by an event other than `pull_request`
251-
this.hasSingleTrigger("workflow_call") and
252-
exists(ExternalJob call, Workflow caller |
253-
call.getCallee() = this.getLocation().getFile().getRelativePath() and
254-
caller = call.getWorkflow() and
255-
caller.isPrivileged()
256-
)
257-
or
258-
// The Workflow has multiple triggers so at least one is ont "pull_request"
259-
count(this.getATriggerEvent()) > 1
260-
}
231+
predicate isPrivileged() { super.isPrivileged() }
261232
}
262233

263234
class ReusableWorkflow extends Workflow instanceof ReusableWorkflowImpl {
@@ -325,6 +296,8 @@ abstract class Job extends AstNode instanceof JobImpl {
325296
Permissions getPermissions() { result = super.getPermissions() }
326297

327298
Strategy getStrategy() { result = super.getStrategy() }
299+
300+
predicate isPrivileged() { super.isPrivileged() }
328301
}
329302

330303
class LocalJob extends Job instanceof LocalJobImpl {

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
private import codeql.actions.ast.internal.Yaml
22
private import codeql.Locations
33
private import codeql.actions.Ast::Utils as Utils
4+
private import codeql.actions.dataflow.ExternalFlow
45

56
/**
67
* Gets the length of each line in the StringValue .
@@ -332,8 +333,40 @@ class WorkflowImpl extends AstNodeImpl, TWorkflowNode {
332333
/** Gets the permissions granted to this workflow. */
333334
PermissionsImpl getPermissions() { result.getNode() = n.lookup("permissions") }
334335

336+
private predicate hasSingleTrigger(string trigger) {
337+
this.getATriggerEvent() = trigger and
338+
count(this.getATriggerEvent()) = 1
339+
}
340+
335341
/** Gets the strategy for this workflow. */
336342
StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") }
343+
344+
/** Holds if the workflow is privileged. */
345+
predicate isPrivileged() {
346+
// The Workflow has a permission to write to some scope
347+
this.getPermissions().getAPermission() = "write"
348+
or
349+
// The Workflow accesses a secret
350+
exists(SecretsExpressionImpl expr |
351+
expr.getEnclosingWorkflow() = this and not expr.getFieldName() = "GITHUB_TOKEN"
352+
)
353+
or
354+
// The Workflow is triggered by an event other than `pull_request`
355+
count(this.getATriggerEvent()) = 1 and
356+
not this.getATriggerEvent() = ["pull_request", "workflow_call"]
357+
or
358+
// The Workflow is only triggered by `workflow_call` and there is
359+
// a caller workflow triggered by an event other than `pull_request`
360+
this.hasSingleTrigger("workflow_call") and
361+
exists(ExternalJobImpl call, WorkflowImpl caller |
362+
call.getCallee() = this.getLocation().getFile().getRelativePath() and
363+
caller = call.getWorkflow() and
364+
caller.isPrivileged()
365+
)
366+
or
367+
// The Workflow has multiple triggers so at least one is not "pull_request"
368+
count(this.getATriggerEvent()) > 1
369+
}
337370
}
338371

339372
class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl {
@@ -597,6 +630,36 @@ class JobImpl extends AstNodeImpl, TJobNode {
597630

598631
/** Gets the strategy for this job. */
599632
StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") }
633+
634+
/** Holds if the workflow is privileged. */
635+
predicate isPrivileged() {
636+
// The job has a permission to write to some scope
637+
this.getPermissions().getAPermission() = "write"
638+
or
639+
// The job accesses a secret
640+
exists(SecretsExpressionImpl expr |
641+
expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN"
642+
)
643+
or
644+
// The effective permissions have write access
645+
exists(string path, string name, string secrets_source, string perms |
646+
workflowDataModel(path, _, name, secrets_source, perms, _) and
647+
path.trim() = this.getLocation().getFile().getRelativePath() and
648+
name.trim().matches(this.getId() + "%") and
649+
(
650+
secrets_source.trim().toLowerCase() = "actions" or
651+
perms.toLowerCase().matches("%write%")
652+
)
653+
)
654+
or
655+
// The job has no expliclit permission, but the enclosing workflow is privileged
656+
not exists(this.getPermissions()) and
657+
not exists(SecretsExpressionImpl expr |
658+
expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN"
659+
) and
660+
// The enclosing workflow is privileged
661+
this.getEnclosingWorkflow().isPrivileged()
662+
}
600663
}
601664

602665
class LocalJobImpl extends JobImpl {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ private import internal.ExternalFlowExtensions as Extensions
22
private import codeql.actions.DataFlow
33
private import actions
44

5+
predicate workflowDataModel(
6+
string path, string visibility, string job, string secrets_source, string permissions,
7+
string runner
8+
) {
9+
Extensions::workflowDataModel(path, visibility, job, secrets_source, permissions, runner)
10+
}
11+
512
/**
613
* MaD sources
714
* Fields:

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ extensible predicate summaryModel(
2222
extensible predicate sinkModel(
2323
string action, string version, string input, string kind, string provenance
2424
);
25+
26+
extensible predicate workflowDataModel(
27+
string path, string visibility, string job, string secrets_source, string permissions,
28+
string runner
29+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
extensions:
2+
- addsTo:
3+
pack: githubsecuritylab/actions-all
4+
extensible: workflowDataModel
5+
data: []

ql/lib/qlpack.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
library: true
33
warnOnImplicitThis: true
44
name: githubsecuritylab/actions-all
5-
version: 0.0.15
5+
version: 0.0.16
66
dependencies:
77
codeql/util: ^0.2.0
88
codeql/yaml: ^0.1.2
@@ -15,3 +15,4 @@ groups:
1515
dataExtensions:
1616
- ext/*.model.yml
1717
- ext/**/*.model.yml
18+
- ext/workflow-models/workflow-models.yml

ql/src/Security/CWE-077/EnvPathInjection.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ from EnvPathInjectionFlow::PathNode source, EnvPathInjectionFlow::PathNode sink
2020
where
2121
EnvPathInjectionFlow::flowPath(source, sink) and
2222
(
23-
exists(source.getNode().asExpr().getEnclosingCompositeAction())
23+
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
2424
or
25-
exists(Workflow w |
26-
w = source.getNode().asExpr().getEnclosingWorkflow() and
27-
not w.isPrivileged()
25+
exists(Job j |
26+
j = sink.getNode().asExpr().getEnclosingJob() and
27+
not j.isPrivileged()
2828
)
2929
)
3030
select sink.getNode(), source, sink,

ql/src/Security/CWE-077/EnvVarInjection.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
2020
where
2121
EnvVarInjectionFlow::flowPath(source, sink) and
2222
(
23-
exists(source.getNode().asExpr().getEnclosingCompositeAction())
23+
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
2424
or
25-
exists(Workflow w |
26-
w = source.getNode().asExpr().getEnclosingWorkflow() and
27-
not w.isPrivileged()
25+
exists(Job j |
26+
j = sink.getNode().asExpr().getEnclosingJob() and
27+
not j.isPrivileged()
2828
)
2929
)
3030
select sink.getNode(), source, sink,

ql/src/Security/CWE-077/PrivilegedEnvPathInjection.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import EnvPathInjectionFlow::PathGraph
1919
from EnvPathInjectionFlow::PathNode source, EnvPathInjectionFlow::PathNode sink
2020
where
2121
EnvPathInjectionFlow::flowPath(source, sink) and
22-
exists(Workflow w |
23-
w = source.getNode().asExpr().getEnclosingWorkflow() and
24-
w.isPrivileged()
22+
exists(Job j |
23+
j = sink.getNode().asExpr().getEnclosingJob() and
24+
j.isPrivileged()
2525
)
2626
select sink.getNode(), source, sink,
2727
"Potential privileged PATH environment variable injection in $@, which may be controlled by an external user.",

ql/src/Security/CWE-077/PrivilegedEnvVarInjection.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import EnvVarInjectionFlow::PathGraph
1919
from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
2020
where
2121
EnvVarInjectionFlow::flowPath(source, sink) and
22-
exists(Workflow w |
23-
w = source.getNode().asExpr().getEnclosingWorkflow() and
24-
w.isPrivileged()
22+
exists(Job j |
23+
j = sink.getNode().asExpr().getEnclosingJob() and
24+
j.isPrivileged()
2525
)
2626
select sink.getNode(), source, sink,
2727
"Potential privileged environment variable injection in $@, which may be controlled by an external user.",

ql/src/Security/CWE-078/CommandInjection.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
2020
where
2121
CommandInjectionFlow::flowPath(source, sink) and
2222
(
23-
exists(source.getNode().asExpr().getEnclosingCompositeAction())
23+
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
2424
or
25-
exists(Workflow w |
26-
w = source.getNode().asExpr().getEnclosingWorkflow() and
27-
not w.isPrivileged()
25+
exists(Job j |
26+
j = sink.getNode().asExpr().getEnclosingJob() and
27+
not j.isPrivileged()
2828
)
2929
)
3030
select sink.getNode(), source, sink,

ql/src/Security/CWE-078/PrivilegedCommandInjection.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import CommandInjectionFlow::PathGraph
1919
from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
2020
where
2121
CommandInjectionFlow::flowPath(source, sink) and
22-
exists(Workflow w |
23-
w = source.getNode().asExpr().getEnclosingWorkflow() and
24-
w.isPrivileged()
22+
exists(Job j |
23+
j = sink.getNode().asExpr().getEnclosingJob() and
24+
j.isPrivileged()
2525
)
2626
select sink.getNode(), source, sink,
2727
"Potential privileged command injection in $@, which may be controlled by an external user.",

ql/src/Security/CWE-094/CodeInjection.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
2222
where
2323
CodeInjectionFlow::flowPath(source, sink) and
2424
(
25-
exists(source.getNode().asExpr().getEnclosingCompositeAction())
25+
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
2626
or
27-
exists(Workflow w |
28-
w = source.getNode().asExpr().getEnclosingWorkflow() and
29-
not w.isPrivileged()
27+
exists(Job j |
28+
j = sink.getNode().asExpr().getEnclosingJob() and
29+
not j.isPrivileged()
3030
)
3131
)
3232
select sink.getNode(), source, sink,

ql/src/Security/CWE-094/PrivilegedCodeInjection.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import CodeInjectionFlow::PathGraph
2121
from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
2222
where
2323
CodeInjectionFlow::flowPath(source, sink) and
24-
exists(Workflow w |
25-
w = source.getNode().asExpr().getEnclosingWorkflow() and
26-
w.isPrivileged()
24+
exists(Job j |
25+
j = sink.getNode().asExpr().getEnclosingJob() and
26+
j.isPrivileged()
2727
)
2828
select sink.getNode(), source, sink,
2929
"Potential privileged code injection in $@, which may be controlled by an external user.", sink,

ql/src/Security/CWE-829/ArtifactPoisoning.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ from ArtifactPoisoningFlow::PathNode source, ArtifactPoisoningFlow::PathNode sin
1919
where
2020
ArtifactPoisoningFlow::flowPath(source, sink) and
2121
(
22-
exists(source.getNode().asExpr().getEnclosingCompositeAction())
22+
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
2323
or
24-
exists(Workflow w |
25-
w = source.getNode().asExpr().getEnclosingWorkflow() and
26-
not w.isPrivileged()
24+
exists(Job j |
25+
j = sink.getNode().asExpr().getEnclosingJob() and
26+
not j.isPrivileged()
2727
)
2828
)
2929
select sink.getNode(), source, sink,

ql/src/Security/CWE-829/PrivilegedArtifactPoisoning.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import ArtifactPoisoningFlow::PathGraph
1818
from ArtifactPoisoningFlow::PathNode source, ArtifactPoisoningFlow::PathNode sink
1919
where
2020
ArtifactPoisoningFlow::flowPath(source, sink) and
21-
exists(Workflow w |
22-
w = source.getNode().asExpr().getEnclosingWorkflow() and
23-
w.isPrivileged()
21+
exists(Job j |
22+
j = sink.getNode().asExpr().getEnclosingJob() and
23+
j.isPrivileged()
2424
)
2525
select sink.getNode(), source, sink,
2626
"Potential privileged artifact poisoning in $@, which may be controlled by an external user.",

ql/src/qlpack.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
library: false
33
name: githubsecuritylab/actions-queries
4-
version: 0.0.15
4+
version: 0.0.16
55
groups:
66
- actions
77
- queries

ql/test/library-tests/workflowenum.expected

Whitespace-only changes.

0 commit comments

Comments
 (0)