mirror of
https://github.com/github/codeql.git
synced 2026-01-10 13:10:26 +01:00
Merge pull request #1 from github/extensionpack
Support external workflow extpacks
This commit is contained in:
7
.github/action/src/codeql.ts
vendored
7
.github/action/src/codeql.ts
vendored
@@ -147,6 +147,13 @@ export async function codeqlDatabaseAnalyze(
|
||||
codeql_output,
|
||||
];
|
||||
|
||||
const extPackPath = process.env["EXTPACK_PATH"];
|
||||
const extPackName = process.env["EXTPACK_NAME"];
|
||||
if (extPackPath !== undefined && extPackName !== undefined) {
|
||||
cmd.push("--additional-packs", extPackPath);
|
||||
cmd.push("--extension-packs", extPackName);
|
||||
}
|
||||
|
||||
// remote pack or local pack
|
||||
if (codeql.pack.startsWith("githubsecuritylab/")) {
|
||||
var suite = codeql.pack + ":" + codeql.suite;
|
||||
|
||||
35
action.yml
35
action.yml
@@ -18,10 +18,41 @@ inputs:
|
||||
description: "CodeQL Suite to run"
|
||||
default: "actions-code-scanning"
|
||||
|
||||
workflow-models:
|
||||
description: "Workflow models"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Do something with context
|
||||
- name: Process workflow models
|
||||
shell: bash
|
||||
if: inputs.workflow-models
|
||||
run: |
|
||||
// Create QLPack directory
|
||||
mkdir workflow-extpack
|
||||
cd workflow-extpack
|
||||
|
||||
// Store the extension pack file
|
||||
cat > models.yml << 'EOF'
|
||||
${{ inputs.workflow-models }}
|
||||
EOF
|
||||
|
||||
// Create QLPack
|
||||
cat > qlpack.yml << 'EOF'
|
||||
name: local/workflow-models
|
||||
library: true
|
||||
extensionTargets:
|
||||
githubsecuritylab/actions-all: '*'
|
||||
dataExtensions:
|
||||
- models.yml
|
||||
EOF
|
||||
|
||||
// Set env vars
|
||||
echo "EXTPACK_PATH=./workflow-extpack" >> $GITHUB_ENV
|
||||
echo "EXTPACK_NAME=local/workflow-models" >> $GITHUB_ENV
|
||||
|
||||
- name: Scan workflows
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
||||
@@ -29,5 +60,7 @@ runs:
|
||||
INPUT_SOURCE-ROOT: ${{ inputs.source-root }}
|
||||
INPUT_SARIF-OUTPUT: ${{ inputs.sarif-output }}
|
||||
INPUT_SUITE: ${{ inputs.suite }}
|
||||
EXTPACK_PATH: ${{ inputs.extpack-path }}
|
||||
EXTPACK_NAME: ${{ inputs.extpack-name }}
|
||||
run: |
|
||||
node ${{ github.action_path }}/.github/action/dist/index.js
|
||||
|
||||
@@ -228,36 +228,7 @@ class Workflow extends AstNode instanceof WorkflowImpl {
|
||||
|
||||
Strategy getStrategy() { result = super.getStrategy() }
|
||||
|
||||
predicate hasSingleTrigger(string trigger) {
|
||||
this.getATriggerEvent() = trigger and
|
||||
count(this.getATriggerEvent()) = 1
|
||||
}
|
||||
|
||||
predicate isPrivileged() {
|
||||
// The Workflow has a permission to write to some scope
|
||||
this.getPermissions().getAPermission() = "write"
|
||||
or
|
||||
// The Workflow accesses a secret
|
||||
exists(SecretsExpression expr |
|
||||
expr.getEnclosingWorkflow() = this and not expr.getFieldName() = "GITHUB_TOKEN"
|
||||
)
|
||||
or
|
||||
// The Workflow is triggered by an event other than `pull_request`
|
||||
count(this.getATriggerEvent()) = 1 and
|
||||
not this.getATriggerEvent() = ["pull_request", "workflow_call"]
|
||||
or
|
||||
// The Workflow is only triggered by `workflow_call` and there is
|
||||
// a caller workflow triggered by an event other than `pull_request`
|
||||
this.hasSingleTrigger("workflow_call") and
|
||||
exists(ExternalJob call, Workflow caller |
|
||||
call.getCallee() = this.getLocation().getFile().getRelativePath() and
|
||||
caller = call.getWorkflow() and
|
||||
caller.isPrivileged()
|
||||
)
|
||||
or
|
||||
// The Workflow has multiple triggers so at least one is ont "pull_request"
|
||||
count(this.getATriggerEvent()) > 1
|
||||
}
|
||||
predicate isPrivileged() { super.isPrivileged() }
|
||||
}
|
||||
|
||||
class ReusableWorkflow extends Workflow instanceof ReusableWorkflowImpl {
|
||||
@@ -325,6 +296,8 @@ abstract class Job extends AstNode instanceof JobImpl {
|
||||
Permissions getPermissions() { result = super.getPermissions() }
|
||||
|
||||
Strategy getStrategy() { result = super.getStrategy() }
|
||||
|
||||
predicate isPrivileged() { super.isPrivileged() }
|
||||
}
|
||||
|
||||
class LocalJob extends Job instanceof LocalJobImpl {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
private import codeql.actions.ast.internal.Yaml
|
||||
private import codeql.Locations
|
||||
private import codeql.actions.Ast::Utils as Utils
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
|
||||
/**
|
||||
* Gets the length of each line in the StringValue .
|
||||
@@ -332,8 +333,40 @@ class WorkflowImpl extends AstNodeImpl, TWorkflowNode {
|
||||
/** Gets the permissions granted to this workflow. */
|
||||
PermissionsImpl getPermissions() { result.getNode() = n.lookup("permissions") }
|
||||
|
||||
private predicate hasSingleTrigger(string trigger) {
|
||||
this.getATriggerEvent() = trigger and
|
||||
count(this.getATriggerEvent()) = 1
|
||||
}
|
||||
|
||||
/** Gets the strategy for this workflow. */
|
||||
StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") }
|
||||
|
||||
/** Holds if the workflow is privileged. */
|
||||
predicate isPrivileged() {
|
||||
// The Workflow has a permission to write to some scope
|
||||
this.getPermissions().getAPermission() = "write"
|
||||
or
|
||||
// The Workflow accesses a secret
|
||||
exists(SecretsExpressionImpl expr |
|
||||
expr.getEnclosingWorkflow() = this and not expr.getFieldName() = "GITHUB_TOKEN"
|
||||
)
|
||||
or
|
||||
// The Workflow is triggered by an event other than `pull_request`
|
||||
count(this.getATriggerEvent()) = 1 and
|
||||
not this.getATriggerEvent() = ["pull_request", "workflow_call"]
|
||||
or
|
||||
// The Workflow is only triggered by `workflow_call` and there is
|
||||
// a caller workflow triggered by an event other than `pull_request`
|
||||
this.hasSingleTrigger("workflow_call") and
|
||||
exists(ExternalJobImpl call, WorkflowImpl caller |
|
||||
call.getCallee() = this.getLocation().getFile().getRelativePath() and
|
||||
caller = call.getWorkflow() and
|
||||
caller.isPrivileged()
|
||||
)
|
||||
or
|
||||
// The Workflow has multiple triggers so at least one is not "pull_request"
|
||||
count(this.getATriggerEvent()) > 1
|
||||
}
|
||||
}
|
||||
|
||||
class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl {
|
||||
@@ -597,6 +630,36 @@ class JobImpl extends AstNodeImpl, TJobNode {
|
||||
|
||||
/** Gets the strategy for this job. */
|
||||
StrategyImpl getStrategy() { result.getNode() = n.lookup("strategy") }
|
||||
|
||||
/** Holds if the workflow is privileged. */
|
||||
predicate isPrivileged() {
|
||||
// The job has a permission to write to some scope
|
||||
this.getPermissions().getAPermission() = "write"
|
||||
or
|
||||
// The job accesses a secret
|
||||
exists(SecretsExpressionImpl expr |
|
||||
expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN"
|
||||
)
|
||||
or
|
||||
// The effective permissions have write access
|
||||
exists(string path, string name, string secrets_source, string perms |
|
||||
workflowDataModel(path, _, name, secrets_source, perms, _) and
|
||||
path.trim() = this.getLocation().getFile().getRelativePath() and
|
||||
name.trim().matches(this.getId() + "%") and
|
||||
(
|
||||
secrets_source.trim().toLowerCase() = "actions" or
|
||||
perms.toLowerCase().matches("%write%")
|
||||
)
|
||||
)
|
||||
or
|
||||
// The job has no expliclit permission, but the enclosing workflow is privileged
|
||||
not exists(this.getPermissions()) and
|
||||
not exists(SecretsExpressionImpl expr |
|
||||
expr.getEnclosingJob() = this and not expr.getFieldName() = "GITHUB_TOKEN"
|
||||
) and
|
||||
// The enclosing workflow is privileged
|
||||
this.getEnclosingWorkflow().isPrivileged()
|
||||
}
|
||||
}
|
||||
|
||||
class LocalJobImpl extends JobImpl {
|
||||
|
||||
@@ -2,6 +2,13 @@ private import internal.ExternalFlowExtensions as Extensions
|
||||
private import codeql.actions.DataFlow
|
||||
private import actions
|
||||
|
||||
predicate workflowDataModel(
|
||||
string path, string visibility, string job, string secrets_source, string permissions,
|
||||
string runner
|
||||
) {
|
||||
Extensions::workflowDataModel(path, visibility, job, secrets_source, permissions, runner)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD sources
|
||||
* Fields:
|
||||
|
||||
@@ -22,3 +22,8 @@ extensible predicate summaryModel(
|
||||
extensible predicate sinkModel(
|
||||
string action, string version, string input, string kind, string provenance
|
||||
);
|
||||
|
||||
extensible predicate workflowDataModel(
|
||||
string path, string visibility, string job, string secrets_source, string permissions,
|
||||
string runner
|
||||
);
|
||||
|
||||
5
ql/lib/ext/workflow-models/workflow-models.yml
Normal file
5
ql/lib/ext/workflow-models/workflow-models.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: githubsecuritylab/actions-all
|
||||
extensible: workflowDataModel
|
||||
data: []
|
||||
@@ -2,7 +2,7 @@
|
||||
library: true
|
||||
warnOnImplicitThis: true
|
||||
name: githubsecuritylab/actions-all
|
||||
version: 0.0.15
|
||||
version: 0.0.16
|
||||
dependencies:
|
||||
codeql/util: ^0.2.0
|
||||
codeql/yaml: ^0.1.2
|
||||
@@ -15,3 +15,4 @@ groups:
|
||||
dataExtensions:
|
||||
- ext/*.model.yml
|
||||
- ext/**/*.model.yml
|
||||
- ext/workflow-models/workflow-models.yml
|
||||
|
||||
@@ -20,11 +20,11 @@ from EnvPathInjectionFlow::PathNode source, EnvPathInjectionFlow::PathNode sink
|
||||
where
|
||||
EnvPathInjectionFlow::flowPath(source, sink) and
|
||||
(
|
||||
exists(source.getNode().asExpr().getEnclosingCompositeAction())
|
||||
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
|
||||
or
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
not w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
not j.isPrivileged()
|
||||
)
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
|
||||
@@ -20,11 +20,11 @@ from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
|
||||
where
|
||||
EnvVarInjectionFlow::flowPath(source, sink) and
|
||||
(
|
||||
exists(source.getNode().asExpr().getEnclosingCompositeAction())
|
||||
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
|
||||
or
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
not w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
not j.isPrivileged()
|
||||
)
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
|
||||
@@ -19,9 +19,9 @@ import EnvPathInjectionFlow::PathGraph
|
||||
from EnvPathInjectionFlow::PathNode source, EnvPathInjectionFlow::PathNode sink
|
||||
where
|
||||
EnvPathInjectionFlow::flowPath(source, sink) and
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
j.isPrivileged()
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
"Potential privileged PATH environment variable injection in $@, which may be controlled by an external user.",
|
||||
|
||||
@@ -19,9 +19,9 @@ import EnvVarInjectionFlow::PathGraph
|
||||
from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
|
||||
where
|
||||
EnvVarInjectionFlow::flowPath(source, sink) and
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
j.isPrivileged()
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
"Potential privileged environment variable injection in $@, which may be controlled by an external user.",
|
||||
|
||||
@@ -20,11 +20,11 @@ from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
|
||||
where
|
||||
CommandInjectionFlow::flowPath(source, sink) and
|
||||
(
|
||||
exists(source.getNode().asExpr().getEnclosingCompositeAction())
|
||||
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
|
||||
or
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
not w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
not j.isPrivileged()
|
||||
)
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
|
||||
@@ -19,9 +19,9 @@ import CommandInjectionFlow::PathGraph
|
||||
from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
|
||||
where
|
||||
CommandInjectionFlow::flowPath(source, sink) and
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
j.isPrivileged()
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
"Potential privileged command injection in $@, which may be controlled by an external user.",
|
||||
|
||||
@@ -22,11 +22,11 @@ from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
|
||||
where
|
||||
CodeInjectionFlow::flowPath(source, sink) and
|
||||
(
|
||||
exists(source.getNode().asExpr().getEnclosingCompositeAction())
|
||||
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
|
||||
or
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
not w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
not j.isPrivileged()
|
||||
)
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
|
||||
@@ -21,9 +21,9 @@ import CodeInjectionFlow::PathGraph
|
||||
from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
|
||||
where
|
||||
CodeInjectionFlow::flowPath(source, sink) and
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
j.isPrivileged()
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
"Potential privileged code injection in $@, which may be controlled by an external user.", sink,
|
||||
|
||||
@@ -19,11 +19,11 @@ from ArtifactPoisoningFlow::PathNode source, ArtifactPoisoningFlow::PathNode sin
|
||||
where
|
||||
ArtifactPoisoningFlow::flowPath(source, sink) and
|
||||
(
|
||||
exists(source.getNode().asExpr().getEnclosingCompositeAction())
|
||||
exists(sink.getNode().asExpr().getEnclosingCompositeAction())
|
||||
or
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
not w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
not j.isPrivileged()
|
||||
)
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
|
||||
@@ -18,9 +18,9 @@ import ArtifactPoisoningFlow::PathGraph
|
||||
from ArtifactPoisoningFlow::PathNode source, ArtifactPoisoningFlow::PathNode sink
|
||||
where
|
||||
ArtifactPoisoningFlow::flowPath(source, sink) and
|
||||
exists(Workflow w |
|
||||
w = source.getNode().asExpr().getEnclosingWorkflow() and
|
||||
w.isPrivileged()
|
||||
exists(Job j |
|
||||
j = sink.getNode().asExpr().getEnclosingJob() and
|
||||
j.isPrivileged()
|
||||
)
|
||||
select sink.getNode(), source, sink,
|
||||
"Potential privileged artifact poisoning in $@, which may be controlled by an external user.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
library: false
|
||||
name: githubsecuritylab/actions-queries
|
||||
version: 0.0.15
|
||||
version: 0.0.16
|
||||
groups:
|
||||
- actions
|
||||
- queries
|
||||
|
||||
0
ql/test/library-tests/workflowenum.expected
Normal file
0
ql/test/library-tests/workflowenum.expected
Normal file
8
ql/test/library-tests/workflowenum.ql
Normal file
8
ql/test/library-tests/workflowenum.ql
Normal file
@@ -0,0 +1,8 @@
|
||||
import actions
|
||||
import codeql.actions.dataflow.internal.ExternalFlowExtensions as Extensions
|
||||
|
||||
from
|
||||
string path, string visibility, string job, string secrets_source, string permissions,
|
||||
string runner
|
||||
where Extensions::workflowDataModel(path, visibility, job, secrets_source, permissions, runner)
|
||||
select visibility, path, job, secrets_source, permissions, runner
|
||||
53
ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning61.yml
vendored
Normal file
53
ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning61.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Dependency Tree Reporter
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Dependency Tree Input Builder" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
compare:
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
${{ github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{github.event.workflow_run.id }},
|
||||
});
|
||||
console.log(artifacts);
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "input-artifacts"
|
||||
})[0];
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/input.zip', Buffer.from(download.data));
|
||||
- name: Set needed env vars in outputs
|
||||
id: prepare
|
||||
run: |
|
||||
unzip input.zip
|
||||
echo current directory contents
|
||||
ls -al
|
||||
|
||||
echo Reading PR number
|
||||
tmp=$(<pr)
|
||||
echo "PR: ${tmp}"
|
||||
echo "pr=${tmp}" >> $GITHUB_OUTPUT
|
||||
|
||||
- run: echo ${{ steps.prepare.outputs.pr }}
|
||||
Reference in New Issue
Block a user