mirror of
https://github.com/github/codeql.git
synced 2026-05-16 04:09:27 +02:00
Compare commits
20 Commits
codeql-cli
...
dataflow/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19119ea0d4 | ||
|
|
134707605b | ||
|
|
1f2cda933d | ||
|
|
5c9e79e947 | ||
|
|
9d6ece1039 | ||
|
|
6ef4aef600 | ||
|
|
9891b412ca | ||
|
|
c71898c265 | ||
|
|
3ae793dd31 | ||
|
|
906a4789f7 | ||
|
|
7265884768 | ||
|
|
333be603d3 | ||
|
|
9907e0d0bf | ||
|
|
6e69b636b9 | ||
|
|
8154500aa5 | ||
|
|
a43b0234b9 | ||
|
|
925fd92485 | ||
|
|
7228766a7c | ||
|
|
15c8968dd4 | ||
|
|
c63283f762 |
15
.bazelrc
15
.bazelrc
@@ -2,9 +2,6 @@ common --enable_platform_specific_config
|
||||
# because we use --override_module with `%workspace%`, the lock file is not stable
|
||||
common --lockfile_mode=off
|
||||
|
||||
# Build release binaries by default, can be overwritten to in local.bazelrc and set to `fastbuild` or `dbg`
|
||||
build --compilation_mode opt
|
||||
|
||||
# when building from this repository in isolation, the internal repository will not be found at ..
|
||||
# where `MODULE.bazel` looks for it. The following will get us past the module loading phase, so
|
||||
# that we can build things that do not rely on that
|
||||
@@ -12,9 +9,6 @@ common --override_module=semmle_code=%workspace%/misc/bazel/semmle_code_stub
|
||||
|
||||
build --repo_env=CC=clang --repo_env=CXX=clang++
|
||||
|
||||
# print test output, like sembuild does.
|
||||
# Set to `errors` if this is too verbose.
|
||||
test --test_output all
|
||||
# we use transitions that break builds of `...`, so for `test` to work with that we need the following
|
||||
test --build_tests_only
|
||||
|
||||
@@ -29,13 +23,6 @@ common --registry=file:///%workspace%/misc/bazel/registry
|
||||
common --registry=https://bcr.bazel.build
|
||||
|
||||
common --@rules_dotnet//dotnet/settings:strict_deps=false
|
||||
|
||||
# Reduce this eventually to empty, once we've fixed all our usages of java, and https://github.com/bazel-contrib/rules_go/issues/4193 is fixed
|
||||
common --incompatible_autoload_externally="+@rules_java,+@rules_shell"
|
||||
|
||||
build --java_language_version=17
|
||||
build --tool_java_language_version=17
|
||||
build --tool_java_runtime_version=remotejdk_17
|
||||
build --java_runtime_version=remotejdk_17
|
||||
common --experimental_isolated_extension_usages
|
||||
|
||||
try-import %workspace%/local.bazelrc
|
||||
|
||||
@@ -8,3 +8,4 @@ common --registry=https://bcr.bazel.build
|
||||
# its implementation packages without providing any code itself.
|
||||
# We either can depend on internal implementation details, or turn of strict deps.
|
||||
common --@rules_dotnet//dotnet/settings:strict_deps=false
|
||||
common --experimental_isolated_extension_usages
|
||||
|
||||
@@ -1 +1 @@
|
||||
8.0.0
|
||||
8.0.0rc1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"bungcip.better-toml",
|
||||
|
||||
9
.devcontainer/swift/Dockerfile
Normal file
9
.devcontainer/swift/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.236.0/containers/cpp/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Debian / Ubuntu version (use Debian 11, Ubuntu 18.04/22.04 on local arm64/Apple Silicon): debian-11, debian-10, ubuntu-22.04, ubuntu-20.04, ubuntu-18.04
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/cpp:0-ubuntu-22.04
|
||||
|
||||
USER root
|
||||
ADD root.sh /tmp/root.sh
|
||||
ADD update-codeql.sh /usr/local/bin/update-codeql
|
||||
RUN bash /tmp/root.sh && rm /tmp/root.sh
|
||||
25
.devcontainer/swift/devcontainer.json
Normal file
25
.devcontainer/swift/devcontainer.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extensions": [
|
||||
"github.vscode-codeql",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"ms-vscode.test-adapter-converter",
|
||||
"slevesque.vscode-zipexplorer",
|
||||
"ms-vscode.cpptools"
|
||||
],
|
||||
"settings": {
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true
|
||||
},
|
||||
"codeQL.runningQueries.memory": 2048
|
||||
},
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
},
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
"remoteUser": "vscode",
|
||||
"onCreateCommand": ".devcontainer/swift/user.sh"
|
||||
}
|
||||
34
.devcontainer/swift/root.sh
Executable file
34
.devcontainer/swift/root.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
set -xe
|
||||
|
||||
BAZELISK_VERSION=v1.12.0
|
||||
BAZELISK_DOWNLOAD_SHA=6b0bcb2ea15bca16fffabe6fda75803440375354c085480fe361d2cbf32501db
|
||||
|
||||
# install git lfs apt source
|
||||
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash
|
||||
|
||||
# install gh apt source
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
|
||||
apt-get update
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get -y install --no-install-recommends \
|
||||
zlib1g-dev \
|
||||
uuid-dev \
|
||||
python3-distutils \
|
||||
python3-pip \
|
||||
bash-completion \
|
||||
git-lfs \
|
||||
gh
|
||||
|
||||
# Install Bazel
|
||||
curl -fSsL -o /usr/local/bin/bazelisk https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64
|
||||
echo "${BAZELISK_DOWNLOAD_SHA} */usr/local/bin/bazelisk" | sha256sum --check -
|
||||
chmod 0755 /usr/local/bin/bazelisk
|
||||
ln -s bazelisk /usr/local/bin/bazel
|
||||
|
||||
# install latest codeql
|
||||
update-codeql
|
||||
20
.devcontainer/swift/update-codeql.sh
Executable file
20
.devcontainer/swift/update-codeql.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
URL=https://github.com/github/codeql-cli-binaries/releases
|
||||
LATEST_VERSION=$(curl -L -s -H 'Accept: application/json' $URL/latest | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/')
|
||||
CURRENT_VERSION=v$(codeql version 2>/dev/null | sed -ne 's/.*release \([0-9.]*\)\./\1/p')
|
||||
if [[ $CURRENT_VERSION != $LATEST_VERSION ]]; then
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "update required, please run this script with sudo:"
|
||||
echo " sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
ZIP=$(mktemp codeql.XXXX.zip)
|
||||
curl -fSqL -o $ZIP $URL/download/$LATEST_VERSION/codeql-linux64.zip
|
||||
unzip -q $ZIP -d /opt
|
||||
rm $ZIP
|
||||
ln -sf /opt/codeql/codeql /usr/local/bin/codeql
|
||||
echo installed version $LATEST_VERSION
|
||||
else
|
||||
echo current version $CURRENT_VERSION is up-to-date
|
||||
fi
|
||||
15
.devcontainer/swift/user.sh
Executable file
15
.devcontainer/swift/user.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
set -xe
|
||||
|
||||
git lfs install
|
||||
|
||||
# add the workspace to the codeql search path
|
||||
mkdir -p /home/vscode/.config/codeql
|
||||
echo "--search-path /workspaces/codeql" > /home/vscode/.config/codeql/config
|
||||
|
||||
# create a swift extractor pack with the current state
|
||||
cd /workspaces/codeql
|
||||
bazel run swift/create-extractor-pack
|
||||
|
||||
#install and set up pre-commit
|
||||
python3 -m pip install pre-commit --no-warn-script-location
|
||||
$HOME/.local/bin/pre-commit install
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -86,5 +86,4 @@
|
||||
/misc/ripunzip/ripunzip-* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# swift prebuilt resources
|
||||
/swift/third_party/resources/*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
/swift/third_party/resources/*.tar.zst filter=lfs diff=lfs merge=lfs -text
|
||||
/swift/third_party/resource-dir/*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
3
.github/codeql/codeql-config.yml
vendored
3
.github/codeql/codeql-config.yml
vendored
@@ -9,4 +9,5 @@ paths-ignore:
|
||||
- '/python/'
|
||||
- '/javascript/ql/test'
|
||||
- '/javascript/extractor/tests'
|
||||
- '/rust/ql'
|
||||
- '/rust/ql/test'
|
||||
- '/rust/ql/integration-tests'
|
||||
|
||||
14
.github/pull_request_template.md
vendored
Normal file
14
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
### Pull Request checklist
|
||||
|
||||
#### All query authors
|
||||
|
||||
- [ ] A change note is added if necessary. See [the documentation](https://github.com/github/codeql/blob/main/docs/change-notes.md) in this repository.
|
||||
- [ ] All new queries have appropriate `.qhelp`. See [the documentation](https://github.com/github/codeql/blob/main/docs/query-help-style-guide.md) in this repository.
|
||||
- [ ] QL tests are added if necessary. See [Testing custom queries](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/testing-custom-queries) in the GitHub documentation.
|
||||
- [ ] New and changed queries have correct query metadata. See [the documentation](https://github.com/github/codeql/blob/main/docs/query-metadata-style-guide.md) in this repository.
|
||||
|
||||
#### Internal query authors only
|
||||
|
||||
- [ ] Autofixes generated based on these changes are valid, only needed if this PR makes significant changes to `.ql`, `.qll`, or `.qhelp` files. See [the documentation](https://github.com/github/codeql-team/blob/main/docs/best-practices/validating-autofix-for-query-changes.md) (internal access required).
|
||||
- [ ] Changes are validated [at scale](https://github.com/github/codeql-dca/) (internal access required).
|
||||
- [ ] Adding a new query? Consider also [adding the query to autofix](https://github.com/github/codeml-autofix/blob/main/docs/updating-query-support.md#adding-a-new-query-to-the-query-suite).
|
||||
3
.github/workflows/check-qldoc.yml
vendored
3
.github/workflows/check-qldoc.yml
vendored
@@ -30,8 +30,7 @@ jobs:
|
||||
run: |
|
||||
EXIT_CODE=0
|
||||
# TODO: remove the shared exception from the regex when coverage of qlpacks without dbschemes is supported
|
||||
# TODO: remove the actions exception once https://github.com/github/codeql-team/issues/3656 is fixed
|
||||
changed_lib_packs="$(git diff --name-only --diff-filter=ACMRT HEAD^ HEAD | { grep -Po '^(?!(shared|actions))[a-z]*/ql/lib' || true; } | sort -u)"
|
||||
changed_lib_packs="$(git diff --name-only --diff-filter=ACMRT HEAD^ HEAD | { grep -Po '^(?!(shared))[a-z]*/ql/lib' || true; } | sort -u)"
|
||||
for pack_dir in ${changed_lib_packs}; do
|
||||
lang="${pack_dir%/ql/lib}"
|
||||
codeql generate library-doc-coverage --output="${RUNNER_TEMP}/${lang}-current.txt" --dir="${pack_dir}"
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.100
|
||||
dotnet-version: 8.0.101
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
9
.github/workflows/compile-queries.yml
vendored
9
.github/workflows/compile-queries.yml
vendored
@@ -7,11 +7,6 @@ on:
|
||||
- "rc/*"
|
||||
- "codeql-cli-*"
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.ql'
|
||||
- '**.qll'
|
||||
- '**/qlpack.yml'
|
||||
- '**.dbscheme'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -38,9 +33,9 @@ jobs:
|
||||
# run with --check-only if running in a PR (github.sha != main)
|
||||
if : ${{ github.event_name == 'pull_request' }}
|
||||
shell: bash
|
||||
run: codeql query compile -q -j0 */ql/{src,examples} --keep-going --warnings=error --check-only --compilation-cache "${{ steps.query-cache.outputs.cache-dir }}" --compilation-cache-size=500 --ram=56000
|
||||
run: codeql query compile -q -j0 */ql/{src,examples} --keep-going --warnings=error --check-only --compilation-cache "${{ steps.query-cache.outputs.cache-dir }}" --compilation-cache-size=500
|
||||
- name: compile queries - full
|
||||
# do full compile if running on main - this populates the cache
|
||||
if : ${{ github.event_name != 'pull_request' }}
|
||||
shell: bash
|
||||
run: codeql query compile -q -j0 */ql/{src,examples} --keep-going --warnings=error --compilation-cache "${{ steps.query-cache.outputs.cache-dir }}" --compilation-cache-size=500 --ram=56000
|
||||
run: codeql query compile -q -j0 */ql/{src,examples} --keep-going --warnings=error --compilation-cache "${{ steps.query-cache.outputs.cache-dir }}" --compilation-cache-size=500
|
||||
|
||||
10
.github/workflows/cpp-swift-analysis.yml
vendored
10
.github/workflows/cpp-swift-analysis.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -38,15 +38,17 @@ jobs:
|
||||
languages: cpp
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Install dependencies
|
||||
- name: "[Ubuntu] Remove GCC 13 from runner image"
|
||||
shell: bash
|
||||
run: |
|
||||
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y uuid-dev
|
||||
sudo apt-get install -y --allow-downgrades libc6=2.35-* libc6-dev=2.35-* libstdc++6=12.3.0-* libgcc-s1=12.3.0-*
|
||||
|
||||
- name: "Build Swift extractor using Bazel"
|
||||
run: |
|
||||
bazel clean --expunge
|
||||
bazel run //swift:install --nouse_action_cache --noremote_accept_cached --noremote_upload_local_results --spawn_strategy=local
|
||||
bazel run //swift:create-extractor-pack --nouse_action_cache --noremote_accept_cached --noremote_upload_local_results --spawn_strategy=local
|
||||
bazel shutdown
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
|
||||
14
.github/workflows/csharp-qltest.yml
vendored
14
.github/workflows/csharp-qltest.yml
vendored
@@ -5,10 +5,8 @@ on:
|
||||
paths:
|
||||
- "csharp/**"
|
||||
- "shared/**"
|
||||
- "misc/bazel/**"
|
||||
- .github/actions/fetch-codeql/action.yml
|
||||
- codeql-workspace.yml
|
||||
- "MODULE.bazel"
|
||||
branches:
|
||||
- main
|
||||
- "rc/*"
|
||||
@@ -16,11 +14,9 @@ on:
|
||||
paths:
|
||||
- "csharp/**"
|
||||
- "shared/**"
|
||||
- "misc/bazel/**"
|
||||
- .github/workflows/csharp-qltest.yml
|
||||
- .github/actions/fetch-codeql/action.yml
|
||||
- codeql-workspace.yml
|
||||
- "MODULE.bazel"
|
||||
branches:
|
||||
- main
|
||||
- "rc/*"
|
||||
@@ -43,14 +39,14 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.100
|
||||
dotnet-version: 8.0.101
|
||||
- name: Extractor unit tests
|
||||
run: |
|
||||
dotnet tool restore
|
||||
dotnet test -p:RuntimeFrameworkVersion=9.0.0 extractor/Semmle.Util.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=9.0.0 extractor/Semmle.Extraction.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=9.0.0 autobuilder/Semmle.Autobuild.CSharp.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=9.0.0 autobuilder/Semmle.Autobuild.Cpp.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=8.0.1 extractor/Semmle.Util.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=8.0.1 extractor/Semmle.Extraction.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=8.0.1 autobuilder/Semmle.Autobuild.CSharp.Tests
|
||||
dotnet test -p:RuntimeFrameworkVersion=8.0.1 autobuilder/Semmle.Autobuild.Cpp.Tests
|
||||
shell: bash
|
||||
stubgentest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
.github/workflows/go-tests-other-os.yml
vendored
1
.github/workflows/go-tests-other-os.yml
vendored
@@ -3,7 +3,6 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "go/**"
|
||||
- "!go/documentation/**"
|
||||
- "!go/ql/**" # don't run other-os if only ql/ files changed
|
||||
- .github/workflows/go-tests-other-os.yml
|
||||
- .github/actions/**
|
||||
|
||||
2
.github/workflows/go-tests.yml
vendored
2
.github/workflows/go-tests.yml
vendored
@@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- "go/**"
|
||||
- "!go/documentation/**"
|
||||
- "shared/**"
|
||||
- .github/workflows/go-tests.yml
|
||||
- .github/actions/**
|
||||
@@ -14,7 +13,6 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "go/**"
|
||||
- "!go/documentation/**"
|
||||
- "shared/**"
|
||||
- .github/workflows/go-tests.yml
|
||||
- .github/actions/**
|
||||
|
||||
2
.github/workflows/ql-for-ql-tests.yml
vendored
2
.github/workflows/ql-for-ql-tests.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
ql/target
|
||||
key: ${{ runner.os }}-${{ steps.os_version.outputs.version }}-qltest-cargo-${{ hashFiles('ql/rust-toolchain.toml', 'ql/**/Cargo.lock') }}
|
||||
- name: Check formatting
|
||||
run: cd ql; cargo fmt -- --check
|
||||
run: cd ql; cargo fmt --all -- --check
|
||||
- name: Build extractor
|
||||
run: |
|
||||
cd ql;
|
||||
|
||||
2
.github/workflows/ruby-build.yml
vendored
2
.github/workflows/ruby-build.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ steps.os_version.outputs.version }}-ruby-rust-cargo-${{ hashFiles('ruby/extractor/rust-toolchain.toml', 'ruby/extractor/**/Cargo.lock') }}
|
||||
- name: Check formatting
|
||||
if: steps.cache-extractor.outputs.cache-hit != 'true'
|
||||
run: cd extractor && cargo fmt -- --check
|
||||
run: cd extractor && cargo fmt --all -- --check
|
||||
- name: Build
|
||||
if: steps.cache-extractor.outputs.cache-hit != 'true'
|
||||
run: cd extractor && cargo build --verbose
|
||||
|
||||
32
.github/workflows/rust.yml
vendored
32
.github/workflows/rust.yml
vendored
@@ -23,48 +23,26 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rust-ast-generator:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust/ast-generator
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Inject sources
|
||||
shell: bash
|
||||
run: |
|
||||
bazel run //rust/ast-generator:inject-sources
|
||||
- name: Format
|
||||
shell: bash
|
||||
run: |
|
||||
cargo fmt --check
|
||||
- name: Compilation
|
||||
shell: bash
|
||||
run: cargo check
|
||||
- name: Clippy
|
||||
shell: bash
|
||||
run: |
|
||||
cargo clippy --no-deps -- -D warnings
|
||||
rust-code:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust/extractor
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Format
|
||||
working-directory: rust/extractor
|
||||
shell: bash
|
||||
run: |
|
||||
cargo fmt --check
|
||||
- name: Compilation
|
||||
working-directory: rust/extractor
|
||||
shell: bash
|
||||
run: cargo check
|
||||
- name: Clippy
|
||||
working-directory: rust/extractor
|
||||
shell: bash
|
||||
run: |
|
||||
cargo clippy --no-deps -- -D warnings
|
||||
cargo clippy --fix
|
||||
git diff --exit-code
|
||||
rust-codegen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
20
.github/workflows/swift.yml
vendored
20
.github/workflows/swift.yml
vendored
@@ -48,6 +48,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./swift/actions/build-and-test
|
||||
build-and-test-linux:
|
||||
if: github.repository_owner == 'github'
|
||||
runs-on: ubuntu-latest-xl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./swift/actions/build-and-test
|
||||
qltests-linux:
|
||||
if: github.repository_owner == 'github'
|
||||
needs: build-and-test-linux
|
||||
runs-on: ubuntu-latest-xl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./swift/actions/run-ql-tests
|
||||
qltests-macos:
|
||||
if: ${{ github.repository_owner == 'github' && github.event_name == 'pull_request' }}
|
||||
needs: build-and-test-macos
|
||||
@@ -96,10 +109,3 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/fetch-codeql
|
||||
- uses: ./swift/actions/database-upgrade-scripts
|
||||
check-no-override:
|
||||
if : github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- shell: bash
|
||||
run: bazel test //swift/... --test_tag_filters=override --test_output=errors
|
||||
|
||||
@@ -32,17 +32,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run clippy
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,7 +8,7 @@
|
||||
|
||||
# qltest projects and artifacts
|
||||
*.actual
|
||||
*/ql/test*/**/*.testproj
|
||||
*/ql/test/**/*.testproj
|
||||
*/ql/test/**/go.sum
|
||||
|
||||
# Visual studio temporaries, except a file used by QL4VS
|
||||
|
||||
@@ -72,7 +72,7 @@ repos:
|
||||
|
||||
- id: rust-codegen
|
||||
name: Run Rust checked in code generation
|
||||
files: ^misc/codegen/|^rust/(prefix\.dbscheme|schema/|codegen/|.*/generated/|ql/lib/(rust\.dbscheme$|codeql/rust/elements)|\.generated.list)
|
||||
files: ^misc/codegen/|^rust/(schema.py$|codegen/|.*/generated/|ql/lib/(rust\.dbscheme$|codeql/rust/elements)|\.generated.list)
|
||||
language: system
|
||||
entry: bazel run //rust/codegen -- --quiet
|
||||
pass_filenames: false
|
||||
|
||||
88
.vscode/tasks.json
vendored
88
.vscode/tasks.json
vendored
@@ -38,94 +38,6 @@
|
||||
"command": "${config:python.pythonPath}",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Create query change note",
|
||||
"type": "process",
|
||||
"command": "python3",
|
||||
"args": [
|
||||
"misc/scripts/create-change-note.py",
|
||||
"${input:language}",
|
||||
"src",
|
||||
"${input:name}",
|
||||
"${input:categoryQuery}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Create library change note",
|
||||
"type": "process",
|
||||
"command": "python3",
|
||||
"args": [
|
||||
"misc/scripts/create-change-note.py",
|
||||
"${input:language}",
|
||||
"lib",
|
||||
"${input:name}",
|
||||
"${input:categoryLibrary}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "pickString",
|
||||
"id": "language",
|
||||
"description": "Language",
|
||||
"options":
|
||||
[
|
||||
"actions",
|
||||
"go",
|
||||
"java",
|
||||
"javascript",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"python",
|
||||
"ruby",
|
||||
"rust",
|
||||
"swift",
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "promptString",
|
||||
"id": "name",
|
||||
"description": "Short name (kebab-case)"
|
||||
},
|
||||
{
|
||||
"type": "pickString",
|
||||
"id": "categoryQuery",
|
||||
"description": "Category (query change)",
|
||||
"options":
|
||||
[
|
||||
"breaking",
|
||||
"deprecated",
|
||||
"newQuery",
|
||||
"queryMetadata",
|
||||
"majorAnalysis",
|
||||
"minorAnalysis",
|
||||
"fix",
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "pickString",
|
||||
"id": "categoryLibrary",
|
||||
"description": "Category (library change)",
|
||||
"options":
|
||||
[
|
||||
"breaking",
|
||||
"deprecated",
|
||||
"feature",
|
||||
"majorAnalysis",
|
||||
"minorAnalysis",
|
||||
"fix",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
exports_files([
|
||||
"LICENSE",
|
||||
"Cargo.lock",
|
||||
"Cargo.toml",
|
||||
])
|
||||
exports_files(["LICENSE"])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/actions/ @github/codeql-dynamic
|
||||
/cpp/ @github/codeql-c-analysis
|
||||
/csharp/ @github/codeql-csharp
|
||||
/csharp/autobuilder/Semmle.Autobuild.Cpp @github/codeql-c-extractor
|
||||
@@ -43,6 +42,3 @@ MODULE.bazel @github/codeql-ci-reviewers
|
||||
# Misc
|
||||
/misc/scripts/accept-expected-changes-from-ci.py @RasmusWL
|
||||
/misc/scripts/generate-code-scanning-query-list.py @RasmusWL
|
||||
|
||||
# .devcontainer
|
||||
/.devcontainer/ @github/codeql-ci-reviewers
|
||||
|
||||
650
Cargo.lock
generated
650
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ members = [
|
||||
"rust/extractor",
|
||||
"rust/extractor/macros",
|
||||
"rust/ast-generator",
|
||||
"rust/autobuild",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2006-2025 GitHub, Inc.
|
||||
Copyright (c) 2006-2020 GitHub, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
173
MODULE.bazel
173
MODULE.bazel
@@ -14,135 +14,61 @@ local_path_override(
|
||||
|
||||
# see https://registry.bazel.build/ for a list of available packages
|
||||
|
||||
bazel_dep(name = "platforms", version = "0.0.11")
|
||||
bazel_dep(name = "rules_go", version = "0.50.1")
|
||||
bazel_dep(name = "platforms", version = "0.0.10")
|
||||
bazel_dep(name = "rules_go", version = "0.50.0")
|
||||
bazel_dep(name = "rules_pkg", version = "1.0.1")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.2.0-codeql.1")
|
||||
bazel_dep(name = "rules_python", version = "0.40.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.3.0")
|
||||
bazel_dep(name = "rules_python", version = "0.36.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.7.1")
|
||||
bazel_dep(name = "abseil-cpp", version = "20240116.1", repo_name = "absl")
|
||||
bazel_dep(name = "abseil-cpp", version = "20240116.0", repo_name = "absl")
|
||||
bazel_dep(name = "nlohmann_json", version = "3.11.3", repo_name = "json")
|
||||
bazel_dep(name = "fmt", version = "10.0.0")
|
||||
bazel_dep(name = "rules_kotlin", version = "2.0.0-codeql.1")
|
||||
bazel_dep(name = "gazelle", version = "0.40.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.17.4")
|
||||
bazel_dep(name = "gazelle", version = "0.38.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.16.1")
|
||||
bazel_dep(name = "googletest", version = "1.14.0.bcr.1")
|
||||
bazel_dep(name = "rules_rust", version = "0.57.1")
|
||||
bazel_dep(name = "zstd", version = "1.5.5.bcr.1")
|
||||
bazel_dep(name = "rules_rust", version = "0.52.2")
|
||||
|
||||
bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True)
|
||||
|
||||
# Keep edition and version approximately in sync with internal repo.
|
||||
# the versions there are canonical, the versions here are used for CI in github/codeql, as well as for the vendoring of dependencies.
|
||||
RUST_EDITION = "2021"
|
||||
|
||||
RUST_VERSION = "1.82.0"
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(
|
||||
edition = RUST_EDITION,
|
||||
# We need those extra target triples so that we can build universal binaries on macos
|
||||
extra_target_triples = [
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin",
|
||||
# crate_py but shortened due to Windows file path considerations
|
||||
cp = use_extension(
|
||||
"@rules_rust//crate_universe:extension.bzl",
|
||||
"crate",
|
||||
isolate = True,
|
||||
)
|
||||
cp.from_cargo(
|
||||
name = "py_deps",
|
||||
cargo_lockfile = "//python/extractor/tsg-python:Cargo.lock",
|
||||
manifests = [
|
||||
"//python/extractor/tsg-python:Cargo.toml",
|
||||
"//python/extractor/tsg-python/tsp:Cargo.toml",
|
||||
],
|
||||
versions = [RUST_VERSION],
|
||||
)
|
||||
use_repo(rust, "rust_toolchains")
|
||||
use_repo(cp, "py_deps")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
# deps for python extractor
|
||||
# keep in sync by running `misc/bazel/3rdparty/update_cargo_deps.sh`
|
||||
py_deps = use_extension("//misc/bazel/3rdparty:py_deps_extension.bzl", "p")
|
||||
use_repo(
|
||||
py_deps,
|
||||
"vendor_py__anyhow-1.0.95",
|
||||
"vendor_py__cc-1.2.14",
|
||||
"vendor_py__clap-4.5.30",
|
||||
"vendor_py__regex-1.11.1",
|
||||
"vendor_py__tree-sitter-0.20.4",
|
||||
"vendor_py__tree-sitter-graph-0.7.0",
|
||||
# deps for ruby+rust, but shortened due to windows file paths
|
||||
r = use_extension(
|
||||
"@rules_rust//crate_universe:extension.bzl",
|
||||
"crate",
|
||||
isolate = True,
|
||||
)
|
||||
|
||||
# deps for ruby+rust
|
||||
# keep in sync by running `misc/bazel/3rdparty/update_cargo_deps.sh`
|
||||
tree_sitter_extractors_deps = use_extension("//misc/bazel/3rdparty:tree_sitter_extractors_extension.bzl", "r")
|
||||
use_repo(
|
||||
tree_sitter_extractors_deps,
|
||||
"vendor__anyhow-1.0.95",
|
||||
"vendor__argfile-0.2.1",
|
||||
"vendor__chrono-0.4.39",
|
||||
"vendor__clap-4.5.26",
|
||||
"vendor__dunce-1.0.5",
|
||||
"vendor__either-1.13.0",
|
||||
"vendor__encoding-0.2.33",
|
||||
"vendor__figment-0.10.19",
|
||||
"vendor__flate2-1.0.35",
|
||||
"vendor__glob-0.3.2",
|
||||
"vendor__globset-0.4.15",
|
||||
"vendor__itertools-0.14.0",
|
||||
"vendor__lazy_static-1.5.0",
|
||||
"vendor__mustache-0.9.0",
|
||||
"vendor__num-traits-0.2.19",
|
||||
"vendor__num_cpus-1.16.0",
|
||||
"vendor__proc-macro2-1.0.93",
|
||||
"vendor__quote-1.0.38",
|
||||
"vendor__ra_ap_base_db-0.0.258",
|
||||
"vendor__ra_ap_cfg-0.0.258",
|
||||
"vendor__ra_ap_hir-0.0.258",
|
||||
"vendor__ra_ap_hir_def-0.0.258",
|
||||
"vendor__ra_ap_hir_expand-0.0.258",
|
||||
"vendor__ra_ap_ide_db-0.0.258",
|
||||
"vendor__ra_ap_intern-0.0.258",
|
||||
"vendor__ra_ap_load-cargo-0.0.258",
|
||||
"vendor__ra_ap_parser-0.0.258",
|
||||
"vendor__ra_ap_paths-0.0.258",
|
||||
"vendor__ra_ap_project_model-0.0.258",
|
||||
"vendor__ra_ap_span-0.0.258",
|
||||
"vendor__ra_ap_stdx-0.0.258",
|
||||
"vendor__ra_ap_syntax-0.0.258",
|
||||
"vendor__ra_ap_vfs-0.0.258",
|
||||
"vendor__rand-0.8.5",
|
||||
"vendor__rayon-1.10.0",
|
||||
"vendor__regex-1.11.1",
|
||||
"vendor__serde-1.0.217",
|
||||
"vendor__serde_json-1.0.135",
|
||||
"vendor__serde_with-3.12.0",
|
||||
"vendor__syn-2.0.96",
|
||||
"vendor__toml-0.8.19",
|
||||
"vendor__tracing-0.1.41",
|
||||
"vendor__tracing-flame-0.2.0",
|
||||
"vendor__tracing-subscriber-0.3.19",
|
||||
"vendor__tree-sitter-0.24.6",
|
||||
"vendor__tree-sitter-embedded-template-0.23.2",
|
||||
"vendor__tree-sitter-json-0.24.8",
|
||||
"vendor__tree-sitter-ql-0.23.1",
|
||||
"vendor__tree-sitter-ruby-0.23.1",
|
||||
"vendor__triomphe-0.1.14",
|
||||
"vendor__ungrammar-1.16.1",
|
||||
)
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# rust-analyzer sources needed by the rust ast-generator (see `rust/ast-generator/README.md`)
|
||||
RUST_ANALYZER_SRC_TAG = "2025-01-07"
|
||||
|
||||
http_archive(
|
||||
name = "rust-analyzer-src",
|
||||
build_file = "//rust/ast-generator:BUILD.rust-analyzer-src.bazel",
|
||||
integrity = "sha256-eo8mIaUafZL8LOM65bDIIIXw1rNQ/P/x5RK/XUtgo5g=",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//rust/ast-generator:patches/rust-analyzer.patch",
|
||||
r.from_cargo(
|
||||
name = "r",
|
||||
cargo_lockfile = "//:Cargo.lock",
|
||||
manifests = [
|
||||
"//:Cargo.toml",
|
||||
"//ruby/extractor:Cargo.toml",
|
||||
"//rust/extractor:Cargo.toml",
|
||||
"//rust/extractor/macros:Cargo.toml",
|
||||
"//rust/ast-generator:Cargo.toml",
|
||||
"//shared/tree-sitter-extractor:Cargo.toml",
|
||||
],
|
||||
strip_prefix = "rust-analyzer-%s" % RUST_ANALYZER_SRC_TAG,
|
||||
url = "https://github.com/rust-lang/rust-analyzer/archive/refs/tags/%s.tar.gz" % RUST_ANALYZER_SRC_TAG,
|
||||
)
|
||||
use_repo(r, tree_sitter_extractors_deps = "r")
|
||||
|
||||
dotnet = use_extension("@rules_dotnet//dotnet:extensions.bzl", "dotnet")
|
||||
dotnet.toolchain(dotnet_version = "9.0.100")
|
||||
dotnet.toolchain(dotnet_version = "8.0.101")
|
||||
use_repo(dotnet, "dotnet_toolchains")
|
||||
|
||||
register_toolchains("@dotnet_toolchains//:all")
|
||||
@@ -165,12 +91,10 @@ use_repo(
|
||||
swift_deps,
|
||||
"binlog",
|
||||
"picosha2",
|
||||
"swift-prebuilt-linux",
|
||||
"swift-prebuilt-linux-download-only",
|
||||
"swift-prebuilt-macos",
|
||||
"swift-prebuilt-macos-download-only",
|
||||
"swift-resource-dir-linux",
|
||||
"swift-resource-dir-macos",
|
||||
"swift_prebuilt_darwin_x86_64",
|
||||
"swift_prebuilt_linux",
|
||||
"swift_toolchain_linux",
|
||||
"swift_toolchain_macos",
|
||||
)
|
||||
|
||||
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
|
||||
@@ -205,7 +129,6 @@ use_repo(
|
||||
"kotlin-compiler-2.0.0-RC1",
|
||||
"kotlin-compiler-2.0.20-Beta2",
|
||||
"kotlin-compiler-2.1.0-Beta1",
|
||||
"kotlin-compiler-2.1.20-Beta1",
|
||||
"kotlin-compiler-embeddable-1.5.0",
|
||||
"kotlin-compiler-embeddable-1.5.10",
|
||||
"kotlin-compiler-embeddable-1.5.20",
|
||||
@@ -220,7 +143,6 @@ use_repo(
|
||||
"kotlin-compiler-embeddable-2.0.0-RC1",
|
||||
"kotlin-compiler-embeddable-2.0.20-Beta2",
|
||||
"kotlin-compiler-embeddable-2.1.0-Beta1",
|
||||
"kotlin-compiler-embeddable-2.1.20-Beta1",
|
||||
"kotlin-stdlib-1.5.0",
|
||||
"kotlin-stdlib-1.5.10",
|
||||
"kotlin-stdlib-1.5.20",
|
||||
@@ -235,11 +157,10 @@ use_repo(
|
||||
"kotlin-stdlib-2.0.0-RC1",
|
||||
"kotlin-stdlib-2.0.20-Beta2",
|
||||
"kotlin-stdlib-2.1.0-Beta1",
|
||||
"kotlin-stdlib-2.1.20-Beta1",
|
||||
)
|
||||
|
||||
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.24.0")
|
||||
go_sdk.download(version = "1.23.1")
|
||||
|
||||
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
|
||||
go_deps.from_file(go_mod = "//go/extractor:go.mod")
|
||||
@@ -265,6 +186,16 @@ lfs_files(
|
||||
executable = True,
|
||||
)
|
||||
|
||||
lfs_files(
|
||||
name = "swift-resource-dir-linux",
|
||||
srcs = ["//swift/third_party/resource-dir:resource-dir-linux.zip"],
|
||||
)
|
||||
|
||||
lfs_files(
|
||||
name = "swift-resource-dir-macos",
|
||||
srcs = ["//swift/third_party/resource-dir:resource-dir-macos.zip"],
|
||||
)
|
||||
|
||||
register_toolchains(
|
||||
"@nodejs_toolchains//:all",
|
||||
)
|
||||
|
||||
@@ -2,8 +2,19 @@ load("//misc/bazel:pkg.bzl", "codeql_pack")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
codeql_pack(
|
||||
name = "actions",
|
||||
srcs = ["//actions/extractor"],
|
||||
experimental = True,
|
||||
)
|
||||
[
|
||||
codeql_pack(
|
||||
name = "-".join(parts),
|
||||
srcs = [
|
||||
"//actions/extractor",
|
||||
],
|
||||
pack_prefix = "/".join(parts),
|
||||
)
|
||||
for parts in (
|
||||
[
|
||||
"experimental",
|
||||
"actions",
|
||||
],
|
||||
["actions"],
|
||||
)
|
||||
]
|
||||
|
||||
@@ -4,9 +4,7 @@ codeql_pkg_files(
|
||||
name = "extractor",
|
||||
srcs = [
|
||||
"codeql-extractor.yml",
|
||||
"//:LICENSE",
|
||||
],
|
||||
exes = glob(["tools/**"]),
|
||||
] + glob(["tools/**"]),
|
||||
strip_prefix = strip_prefix.from_pkg(),
|
||||
visibility = ["//actions:__pkg__"],
|
||||
)
|
||||
|
||||
@@ -2,16 +2,10 @@ if (($null -ne $env:LGTM_INDEX_INCLUDE) -or ($null -ne $env:LGTM_INDEX_EXCLUDE)
|
||||
Write-Output 'Path filters set. Passing them through to the JavaScript extractor.'
|
||||
} else {
|
||||
Write-Output 'No path filters set. Using the default filters.'
|
||||
# Note: We're adding the `reusable_workflows` subdirectories to proactively
|
||||
# record workflows that were called cross-repo, check them out locally,
|
||||
# and enable an interprocedural analysis across the workflow files.
|
||||
# These workflows follow the convention `.github/reusable_workflows/<nwo>/*.ya?ml`
|
||||
$DefaultPathFilters = @(
|
||||
'exclude:**/*',
|
||||
'include:.github/workflows/*.yml',
|
||||
'include:.github/workflows/*.yaml',
|
||||
'include:.github/reusable_workflows/**/*.yml',
|
||||
'include:.github/reusable_workflows/**/*.yaml',
|
||||
'include:.github/workflows/**/*.yml',
|
||||
'include:.github/workflows/**/*.yaml',
|
||||
'include:**/action.yml',
|
||||
'include:**/action.yaml'
|
||||
)
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
|
||||
set -eu
|
||||
|
||||
# Note: We're adding the `reusable_workflows` subdirectories to proactively
|
||||
# record workflows that were called cross-repo, check them out locally,
|
||||
# and enable an interprocedural analysis across the workflow files.
|
||||
# These workflows follow the convention `.github/reusable_workflows/<nwo>/*.ya?ml`
|
||||
DEFAULT_PATH_FILTERS=$(cat << END
|
||||
exclude:**/*
|
||||
include:.github/workflows/*.yml
|
||||
include:.github/workflows/*.yaml
|
||||
include:.github/reusable_workflows/**/*.yml
|
||||
include:.github/reusable_workflows/**/*.yaml
|
||||
include:.github/workflows/**/*.yml
|
||||
include:.github/workflows/**/*.yaml
|
||||
include:**/action.yml
|
||||
include:**/action.yaml
|
||||
END
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: immutableActionsDataModel
|
||||
data:
|
||||
- ["actions/checkout"]
|
||||
- ["actions/cache"]
|
||||
- ["actions/setup-node"]
|
||||
- ["actions/upload-artifact"]
|
||||
- ["actions/setup-python"]
|
||||
- ["actions/download-artifact"]
|
||||
- ["actions/github-script"]
|
||||
- ["actions/setup-java"]
|
||||
- ["actions/setup-go"]
|
||||
- ["actions/upload-pages-artifact"]
|
||||
- ["actions/deploy-pages"]
|
||||
- ["actions/setup-dotnet"]
|
||||
- ["actions/stale"]
|
||||
- ["actions/labeler"]
|
||||
- ["actions/create-github-app-token"]
|
||||
- ["actions/configure-pages"]
|
||||
- ["github/codeql-action/analyze"]
|
||||
- ["github/codeql-action/autobuild"]
|
||||
- ["github/codeql-action/init"]
|
||||
- ["github/codeql-action/resolve-environment"]
|
||||
- ["github/codeql-action/start-proxy"]
|
||||
- ["github/codeql-action/upload-sarif"]
|
||||
- ["octokit/request-action"]
|
||||
@@ -1,14 +0,0 @@
|
||||
# Model pack containing the list of known immutable actions. The Immutable Actions feature is not
|
||||
# yet released, so this pack will only be used within GitHub. Once the feature is available to
|
||||
# customers, we will move the contents of this pack back into the standard library pack.
|
||||
name: codeql/immutable-actions-list
|
||||
version: 0.0.1-dev
|
||||
library: true
|
||||
warnOnImplicitThis: true
|
||||
extensionTargets:
|
||||
# We expect to need this model pack even after GA of Actions analysis, so make it compatible with
|
||||
# all future prereleases plus 1.x.x. We should be able to remove this back before we need to
|
||||
# bump the major version to 2.
|
||||
codeql/actions-all: ">=0.4.3 <2.0.0"
|
||||
dataExtensions:
|
||||
- ext/**/*.yml
|
||||
@@ -1,26 +0,0 @@
|
||||
## 0.4.4
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### New Features
|
||||
|
||||
* The "Unpinned tag for a non-immutable Action in workflow" query (`actions/unpinned-tag`) now supports expanding the trusted action owner list using data extensions (`extensible: trustedActionsOwnerDataModel`). If you trust an Action publisher, you can include the owner name/organization in a model pack to add it to the allow list for this query. This addition will prevent security alerts when using unpinned tags for Actions published by that owner. For more information on creating a model pack, see [Creating a CodeQL Model Pack](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/creating-and-working-with-codeql-packs#creating-a-codeql-model-pack).
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed data for vulnerable versions of `actions/download-artifact` and `rlespinasse/github-slug-action` (following GHSA-cxww-7g56-2vh6 and GHSA-6q4m-7476-932w).
|
||||
* Improved `untrustedGhCommandDataModel` regex for `gh pr view` and Bash taint analysis in GitHub Actions.
|
||||
|
||||
## 0.4.1
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### New Features
|
||||
|
||||
* Initial public preview release
|
||||
@@ -1 +1 @@
|
||||
import codeql.actions.Ast
|
||||
predicate placeholder(int x) { x = 0 }
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
## 0.4.0
|
||||
|
||||
### New Features
|
||||
|
||||
* Initial public preview release
|
||||
@@ -1,3 +0,0 @@
|
||||
## 0.4.1
|
||||
|
||||
No user-facing changes.
|
||||
@@ -1,6 +0,0 @@
|
||||
## 0.4.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed data for vulnerable versions of `actions/download-artifact` and `rlespinasse/github-slug-action` (following GHSA-cxww-7g56-2vh6 and GHSA-6q4m-7476-932w).
|
||||
* Improved `untrustedGhCommandDataModel` regex for `gh pr view` and Bash taint analysis in GitHub Actions.
|
||||
@@ -1,5 +0,0 @@
|
||||
## 0.4.3
|
||||
|
||||
### New Features
|
||||
|
||||
* The "Unpinned tag for a non-immutable Action in workflow" query (`actions/unpinned-tag`) now supports expanding the trusted action owner list using data extensions (`extensible: trustedActionsOwnerDataModel`). If you trust an Action publisher, you can include the owner name/organization in a model pack to add it to the allow list for this query. This addition will prevent security alerts when using unpinned tags for Actions published by that owner. For more information on creating a model pack, see [Creating a CodeQL Model Pack](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/creating-and-working-with-codeql-packs#creating-a-codeql-model-pack).
|
||||
@@ -1,3 +0,0 @@
|
||||
## 0.4.4
|
||||
|
||||
No user-facing changes.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
lockVersion: 1.0.0
|
||||
dependencies: {}
|
||||
compiled: false
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
lastReleaseVersion: 0.4.4
|
||||
@@ -1,98 +0,0 @@
|
||||
/** Provides classes for working with locations. */
|
||||
|
||||
import files.FileSystem
|
||||
import codeql.actions.ast.internal.Ast
|
||||
|
||||
bindingset[loc]
|
||||
pragma[inline_late]
|
||||
private string locationToString(Location loc) {
|
||||
exists(string filepath, int startline, int startcolumn, int endline, int endcolumn |
|
||||
loc.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) and
|
||||
result = filepath + "@" + startline + ":" + startcolumn + ":" + endline + ":" + endcolumn
|
||||
)
|
||||
}
|
||||
|
||||
newtype TLocation =
|
||||
TBaseLocation(string filepath, int startline, int startcolumn, int endline, int endcolumn) {
|
||||
exists(File file |
|
||||
file.getAbsolutePath() = filepath and
|
||||
locations_default(_, file, startline, startcolumn, endline, endcolumn)
|
||||
)
|
||||
or
|
||||
exists(ExpressionImpl e |
|
||||
e.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
)
|
||||
or
|
||||
filepath = "" and startline = 0 and startcolumn = 0 and endline = 0 and endcolumn = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* A location as given by a file, a start line, a start column,
|
||||
* an end line, and an end column.
|
||||
*
|
||||
* For more information about locations see [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
class Location extends TLocation, TBaseLocation {
|
||||
string filepath;
|
||||
int startline;
|
||||
int startcolumn;
|
||||
int endline;
|
||||
int endcolumn;
|
||||
|
||||
Location() { this = TBaseLocation(filepath, startline, startcolumn, endline, endcolumn) }
|
||||
|
||||
/** Gets the file for this location. */
|
||||
File getFile() {
|
||||
exists(File file |
|
||||
file.getAbsolutePath() = filepath and
|
||||
result = file
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the 1-based line number (inclusive) where this location starts. */
|
||||
int getStartLine() { result = startline }
|
||||
|
||||
/** Gets the 1-based column number (inclusive) where this location starts. */
|
||||
int getStartColumn() { result = startcolumn }
|
||||
|
||||
/** Gets the 1-based line number (inclusive) where this.getLocationDefault() location ends. */
|
||||
int getEndLine() { result = endline }
|
||||
|
||||
/** Gets the 1-based column number (inclusive) where this.getLocationDefault() location ends. */
|
||||
int getEndColumn() { result = endcolumn }
|
||||
|
||||
/** Gets the number of lines covered by this location. */
|
||||
int getNumLines() { result = endline - startline + 1 }
|
||||
|
||||
/** Gets a textual representation of this element. */
|
||||
pragma[inline]
|
||||
string toString() { result = locationToString(this) }
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [Providing locations in CodeQL queries](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
predicate hasLocationInfo(string p, int sl, int sc, int el, int ec) {
|
||||
p = filepath and
|
||||
sl = startline and
|
||||
sc = startcolumn and
|
||||
el = endline and
|
||||
ec = endcolumn
|
||||
}
|
||||
|
||||
/** Holds if this location starts strictly before the specified location. */
|
||||
pragma[inline]
|
||||
predicate strictlyBefore(Location other) {
|
||||
this.getStartLine() < other.getStartLine()
|
||||
or
|
||||
this.getStartLine() = other.getStartLine() and this.getStartColumn() < other.getStartColumn()
|
||||
}
|
||||
}
|
||||
|
||||
/** An entity representing an empty location. */
|
||||
class EmptyLocation extends Location {
|
||||
EmptyLocation() { this.hasLocationInfo("", 0, 0, 0, 0) }
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
private import codeql.actions.ast.internal.Ast
|
||||
private import codeql.Locations
|
||||
import codeql.actions.Helper
|
||||
|
||||
class AstNode instanceof AstNodeImpl {
|
||||
AstNode getAChildNode() { result = super.getAChildNode() }
|
||||
|
||||
AstNode getParentNode() { result = super.getParentNode() }
|
||||
|
||||
string getAPrimaryQlClass() { result = super.getAPrimaryQlClass() }
|
||||
|
||||
Location getLocation() { result = super.getLocation() }
|
||||
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
Step getEnclosingStep() { result = super.getEnclosingStep() }
|
||||
|
||||
Job getEnclosingJob() { result = super.getEnclosingJob() }
|
||||
|
||||
Event getATriggerEvent() { result = super.getATriggerEvent() }
|
||||
|
||||
Workflow getEnclosingWorkflow() { result = super.getEnclosingWorkflow() }
|
||||
|
||||
CompositeAction getEnclosingCompositeAction() { result = super.getEnclosingCompositeAction() }
|
||||
|
||||
Expression getInScopeEnvVarExpr(string name) { result = super.getInScopeEnvVarExpr(name) }
|
||||
|
||||
ScalarValue getInScopeDefaultValue(string name, string prop) {
|
||||
result = super.getInScopeDefaultValue(name, prop)
|
||||
}
|
||||
}
|
||||
|
||||
class ScalarValue extends AstNode instanceof ScalarValueImpl {
|
||||
string getValue() { result = super.getValue() }
|
||||
}
|
||||
|
||||
class Expression extends AstNode instanceof ExpressionImpl {
|
||||
string expression;
|
||||
string rawExpression;
|
||||
|
||||
Expression() {
|
||||
expression = this.getExpression() and
|
||||
rawExpression = this.getRawExpression()
|
||||
}
|
||||
|
||||
string getExpression() { result = expression }
|
||||
|
||||
string getRawExpression() { result = rawExpression }
|
||||
|
||||
string getNormalizedExpression() { result = normalizeExpr(expression) }
|
||||
}
|
||||
|
||||
/** A common class for `env` in workflow, job or step. */
|
||||
abstract class Env extends AstNode instanceof EnvImpl {
|
||||
/** Gets an environment variable value given its name. */
|
||||
ScalarValueImpl getEnvVarValue(string name) { result = super.getEnvVarValue(name) }
|
||||
|
||||
/** Gets an environment variable value. */
|
||||
ScalarValueImpl getAnEnvVarValue() { result = super.getAnEnvVarValue() }
|
||||
|
||||
/** Gets an environment variable expressin given its name. */
|
||||
ExpressionImpl getEnvVarExpr(string name) { result = super.getEnvVarExpr(name) }
|
||||
|
||||
/** Gets an environment variable expression. */
|
||||
ExpressionImpl getAnEnvVarExpr() { result = super.getAnEnvVarExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom composite action. This is a mapping at the top level of an Actions YAML action file.
|
||||
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions.
|
||||
*/
|
||||
class CompositeAction extends AstNode instanceof CompositeActionImpl {
|
||||
Runs getRuns() { result = super.getRuns() }
|
||||
|
||||
Outputs getOutputs() { result = super.getOutputs() }
|
||||
|
||||
Expression getAnOutputExpr() { result = super.getAnOutputExpr() }
|
||||
|
||||
Expression getOutputExpr(string outputName) { result = super.getOutputExpr(outputName) }
|
||||
|
||||
Input getAnInput() { result = super.getAnInput() }
|
||||
|
||||
Input getInput(string inputName) { result = super.getInput(inputName) }
|
||||
|
||||
LocalJob getACallerJob() { result = super.getACallerJob() }
|
||||
|
||||
UsesStep getACallerStep() { result = super.getACallerStep() }
|
||||
|
||||
predicate isPrivileged() { super.isPrivileged() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
|
||||
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
|
||||
*/
|
||||
class Workflow extends AstNode instanceof WorkflowImpl {
|
||||
Env getEnv() { result = super.getEnv() }
|
||||
|
||||
string getName() { result = super.getName() }
|
||||
|
||||
Job getAJob() { result = super.getAJob() }
|
||||
|
||||
Job getJob(string jobId) { result = super.getJob(jobId) }
|
||||
|
||||
Permissions getPermissions() { result = super.getPermissions() }
|
||||
|
||||
Strategy getStrategy() { result = super.getStrategy() }
|
||||
|
||||
On getOn() { result = super.getOn() }
|
||||
}
|
||||
|
||||
class ReusableWorkflow extends Workflow instanceof ReusableWorkflowImpl {
|
||||
Outputs getOutputs() { result = super.getOutputs() }
|
||||
|
||||
Expression getAnOutputExpr() { result = super.getAnOutputExpr() }
|
||||
|
||||
Expression getOutputExpr(string outputName) { result = super.getOutputExpr(outputName) }
|
||||
|
||||
Input getAnInput() { result = super.getAnInput() }
|
||||
|
||||
Input getInput(string inputName) { result = super.getInput(inputName) }
|
||||
|
||||
ExternalJob getACaller() { result = super.getACaller() }
|
||||
}
|
||||
|
||||
class Input extends AstNode instanceof InputImpl { }
|
||||
|
||||
class Default extends AstNode instanceof DefaultsImpl {
|
||||
ScalarValue getValue(string name, string prop) { result = super.getValue(name, prop) }
|
||||
}
|
||||
|
||||
class Outputs extends AstNode instanceof OutputsImpl {
|
||||
Expression getAnOutputExpr() { result = super.getAnOutputExpr() }
|
||||
|
||||
Expression getOutputExpr(string outputName) { result = super.getOutputExpr(outputName) }
|
||||
|
||||
override string toString() { result = "Job outputs node" }
|
||||
}
|
||||
|
||||
class Permissions extends AstNode instanceof PermissionsImpl {
|
||||
bindingset[perm]
|
||||
string getPermission(string perm) { result = super.getPermission(perm) }
|
||||
|
||||
string getAPermission() { result = super.getAPermission() }
|
||||
}
|
||||
|
||||
class Strategy extends AstNode instanceof StrategyImpl {
|
||||
Expression getMatrixVarExpr(string varName) { result = super.getMatrixVarExpr(varName) }
|
||||
|
||||
Expression getAMatrixVarExpr() { result = super.getAMatrixVarExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
|
||||
*/
|
||||
class Needs extends AstNode instanceof NeedsImpl {
|
||||
Job getANeededJob() { result = super.getANeededJob() }
|
||||
}
|
||||
|
||||
class On extends AstNode instanceof OnImpl {
|
||||
Event getAnEvent() { result = super.getAnEvent() }
|
||||
}
|
||||
|
||||
class Event extends AstNode instanceof EventImpl {
|
||||
string getName() { result = super.getName() }
|
||||
|
||||
string getAnActivityType() { result = super.getAnActivityType() }
|
||||
|
||||
string getAPropertyValue(string prop) { result = super.getAPropertyValue(prop) }
|
||||
|
||||
predicate hasProperty(string prop) { super.hasProperty(prop) }
|
||||
|
||||
predicate isExternallyTriggerable() { super.isExternallyTriggerable() }
|
||||
|
||||
predicate isPrivileged() { super.isPrivileged() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An Actions job within a workflow.
|
||||
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
|
||||
*/
|
||||
abstract class Job extends AstNode instanceof JobImpl {
|
||||
string getId() { result = super.getId() }
|
||||
|
||||
Workflow getWorkflow() { result = super.getWorkflow() }
|
||||
|
||||
Job getANeededJob() { result = super.getANeededJob() }
|
||||
|
||||
Outputs getOutputs() { result = super.getOutputs() }
|
||||
|
||||
Expression getAnOutputExpr() { result = super.getAnOutputExpr() }
|
||||
|
||||
Expression getOutputExpr(string outputName) { result = super.getOutputExpr(outputName) }
|
||||
|
||||
Env getEnv() { result = super.getEnv() }
|
||||
|
||||
If getIf() { result = super.getIf() }
|
||||
|
||||
Environment getEnvironment() { result = super.getEnvironment() }
|
||||
|
||||
Permissions getPermissions() { result = super.getPermissions() }
|
||||
|
||||
Strategy getStrategy() { result = super.getStrategy() }
|
||||
|
||||
string getARunsOnLabel() { result = super.getARunsOnLabel() }
|
||||
|
||||
predicate isPrivileged() { super.isPrivileged() }
|
||||
|
||||
predicate isPrivilegedExternallyTriggerable(Event event) {
|
||||
super.isPrivilegedExternallyTriggerable(event)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class StepsContainer extends AstNode instanceof StepsContainerImpl {
|
||||
Step getAStep() { result = super.getAStep() }
|
||||
|
||||
Step getStep(int i) { result = super.getStep(i) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An `runs` mapping in a custom composite action YAML.
|
||||
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
|
||||
*/
|
||||
class Runs extends StepsContainer instanceof RunsImpl {
|
||||
CompositeAction getAction() { result = super.getAction() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An Actions job within a workflow which is composed of steps.
|
||||
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
|
||||
*/
|
||||
class LocalJob extends Job, StepsContainer instanceof LocalJobImpl { }
|
||||
|
||||
/**
|
||||
* A step within an Actions job.
|
||||
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps.
|
||||
*/
|
||||
class Step extends AstNode instanceof StepImpl {
|
||||
string getId() { result = super.getId() }
|
||||
|
||||
Env getEnv() { result = super.getEnv() }
|
||||
|
||||
If getIf() { result = super.getIf() }
|
||||
|
||||
StepsContainer getContainer() { result = super.getContainer() }
|
||||
|
||||
Step getNextStep() { result = super.getNextStep() }
|
||||
|
||||
Step getAFollowingStep() { result = super.getAFollowingStep() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An If node representing a conditional statement.
|
||||
*/
|
||||
class If extends AstNode instanceof IfImpl {
|
||||
string getCondition() { result = super.getCondition() }
|
||||
|
||||
Expression getConditionExpr() { result = super.getConditionExpr() }
|
||||
|
||||
string getConditionStyle() { result = super.getConditionStyle() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An Environemnt node representing a deployment environment.
|
||||
*/
|
||||
class Environment extends AstNode instanceof EnvironmentImpl {
|
||||
string getName() { result = super.getName() }
|
||||
|
||||
Expression getNameExpr() { result = super.getNameExpr() }
|
||||
}
|
||||
|
||||
abstract class Uses extends AstNode instanceof UsesImpl {
|
||||
string getCallee() { result = super.getCallee() }
|
||||
|
||||
ScalarValue getCalleeNode() { result = super.getCalleeNode() }
|
||||
|
||||
string getVersion() { result = super.getVersion() }
|
||||
|
||||
int getMajorVersion() { result = super.getMajorVersion() }
|
||||
|
||||
string getArgument(string argName) { result = super.getArgument(argName) }
|
||||
|
||||
Expression getArgumentExpr(string argName) { result = super.getArgumentExpr(argName) }
|
||||
}
|
||||
|
||||
class UsesStep extends Step, Uses instanceof UsesStepImpl { }
|
||||
|
||||
class ExternalJob extends Job, Uses instanceof ExternalJobImpl { }
|
||||
|
||||
/**
|
||||
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
|
||||
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
|
||||
*/
|
||||
class Run extends Step instanceof RunImpl {
|
||||
ShellScript getScript() { result = super.getScript() }
|
||||
|
||||
Expression getAnScriptExpr() { result = super.getAnScriptExpr() }
|
||||
|
||||
string getWorkingDirectory() { result = super.getWorkingDirectory() }
|
||||
|
||||
string getShell() { result = super.getShell() }
|
||||
}
|
||||
|
||||
class ShellScript extends ScalarValueImpl instanceof ShellScriptImpl {
|
||||
string getRawScript() { result = super.getRawScript() }
|
||||
|
||||
string getStmt(int i) { result = super.getStmt(i) }
|
||||
|
||||
string getAStmt() { result = super.getAStmt() }
|
||||
|
||||
string getCommand(int i) { result = super.getCommand(i) }
|
||||
|
||||
string getACommand() { result = super.getACommand() }
|
||||
|
||||
string getFileReadCommand(int i) { result = super.getFileReadCommand(i) }
|
||||
|
||||
string getAFileReadCommand() { result = super.getAFileReadCommand() }
|
||||
|
||||
predicate getAssignment(int i, string name, string data) { super.getAssignment(i, name, data) }
|
||||
|
||||
predicate getAnAssignment(string name, string data) { super.getAnAssignment(name, data) }
|
||||
|
||||
predicate getAWriteToGitHubEnv(string name, string data) {
|
||||
super.getAWriteToGitHubEnv(name, data)
|
||||
}
|
||||
|
||||
predicate getAWriteToGitHubOutput(string name, string data) {
|
||||
super.getAWriteToGitHubOutput(name, data)
|
||||
}
|
||||
|
||||
predicate getAWriteToGitHubPath(string data) { super.getAWriteToGitHubPath(data) }
|
||||
|
||||
predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) {
|
||||
super.getAnEnvReachingGitHubOutputWrite(var, output_field)
|
||||
}
|
||||
|
||||
predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) {
|
||||
super.getACmdReachingGitHubOutputWrite(cmd, output_field)
|
||||
}
|
||||
|
||||
predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) {
|
||||
super.getAnEnvReachingGitHubEnvWrite(var, output_field)
|
||||
}
|
||||
|
||||
predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) {
|
||||
super.getACmdReachingGitHubEnvWrite(cmd, output_field)
|
||||
}
|
||||
|
||||
predicate getAnEnvReachingGitHubPathWrite(string var) {
|
||||
super.getAnEnvReachingGitHubPathWrite(var)
|
||||
}
|
||||
|
||||
predicate getACmdReachingGitHubPathWrite(string cmd) { super.getACmdReachingGitHubPathWrite(cmd) }
|
||||
|
||||
predicate getAnEnvReachingArgumentInjectionSink(string var, string command, string argument) {
|
||||
super.getAnEnvReachingArgumentInjectionSink(var, command, argument)
|
||||
}
|
||||
|
||||
predicate getACmdReachingArgumentInjectionSink(string cmd, string command, string argument) {
|
||||
super.getACmdReachingArgumentInjectionSink(cmd, command, argument)
|
||||
}
|
||||
|
||||
predicate fileToGitHubEnv(string path) { super.fileToGitHubEnv(path) }
|
||||
|
||||
predicate fileToGitHubOutput(string path) { super.fileToGitHubOutput(path) }
|
||||
|
||||
predicate fileToGitHubPath(string path) { super.fileToGitHubPath(path) }
|
||||
}
|
||||
|
||||
abstract class SimpleReferenceExpression extends AstNode instanceof SimpleReferenceExpressionImpl {
|
||||
string getFieldName() { result = super.getFieldName() }
|
||||
|
||||
AstNode getTarget() { result = super.getTarget() }
|
||||
}
|
||||
|
||||
class JsonReferenceExpression extends AstNode instanceof JsonReferenceExpressionImpl {
|
||||
string getAccessPath() { result = super.getAccessPath() }
|
||||
|
||||
string getInnerExpression() { result = super.getInnerExpression() }
|
||||
}
|
||||
|
||||
class GitHubExpression extends SimpleReferenceExpression instanceof GitHubExpressionImpl { }
|
||||
|
||||
class SecretsExpression extends SimpleReferenceExpression instanceof SecretsExpressionImpl { }
|
||||
|
||||
class StepsExpression extends SimpleReferenceExpression instanceof StepsExpressionImpl {
|
||||
string getStepId() { result = super.getStepId() }
|
||||
}
|
||||
|
||||
class NeedsExpression extends SimpleReferenceExpression instanceof NeedsExpressionImpl {
|
||||
string getNeededJobId() { result = super.getNeededJobId() }
|
||||
}
|
||||
|
||||
class JobsExpression extends SimpleReferenceExpression instanceof JobsExpressionImpl { }
|
||||
|
||||
class InputsExpression extends SimpleReferenceExpression instanceof InputsExpressionImpl { }
|
||||
|
||||
class EnvExpression extends SimpleReferenceExpression instanceof EnvExpressionImpl { }
|
||||
|
||||
class MatrixExpression extends SimpleReferenceExpression instanceof MatrixExpressionImpl { }
|
||||
@@ -1,737 +0,0 @@
|
||||
private import codeql.actions.Ast
|
||||
|
||||
class BashShellScript extends ShellScript {
|
||||
BashShellScript() {
|
||||
exists(Run run |
|
||||
this = run.getScript() and
|
||||
run.getShell().matches(["bash%", "sh"])
|
||||
)
|
||||
}
|
||||
|
||||
private string lineProducer(int i) {
|
||||
result = this.getRawScript().regexpReplaceAll("\\\\\\s*\n", "").splitAt("\n", i)
|
||||
}
|
||||
|
||||
private predicate cmdSubstitutionReplacement(string cmdSubs, string id, int k) {
|
||||
exists(string line | line = this.lineProducer(k) |
|
||||
exists(int i, int j |
|
||||
cmdSubs =
|
||||
// $() cmd substitution
|
||||
line.regexpFind("\\$\\((?:[^()]+|\\((?:[^()]+|\\([^()]*\\))*\\))*\\)", i, j)
|
||||
.regexpReplaceAll("^\\$\\(", "")
|
||||
.regexpReplaceAll("\\)$", "") and
|
||||
id = "cmdsubs:" + k + ":" + i + ":" + j
|
||||
)
|
||||
or
|
||||
exists(int i, int j |
|
||||
// `...` cmd substitution
|
||||
cmdSubs =
|
||||
line.regexpFind("\\`[^\\`]+\\`", i, j)
|
||||
.regexpReplaceAll("^\\`", "")
|
||||
.regexpReplaceAll("\\`$", "") and
|
||||
id = "cmd:" + k + ":" + i + ":" + j
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private predicate rankedCmdSubstitutionReplacements(int i, string old, string new) {
|
||||
old = rank[i](string old2 | this.cmdSubstitutionReplacement(old2, _, _) | old2) and
|
||||
this.cmdSubstitutionReplacement(old, new, _)
|
||||
}
|
||||
|
||||
private predicate doReplaceCmdSubstitutions(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.lineProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doReplaceCmdSubstitutions(line, round - 1, old, middle) and
|
||||
this.rankedCmdSubstitutionReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(target, replacement)
|
||||
)
|
||||
}
|
||||
|
||||
private string cmdSubstitutedLineProducer(int i) {
|
||||
// script lines where any command substitution has been replaced with a unique placeholder
|
||||
result =
|
||||
max(int round, string new |
|
||||
this.doReplaceCmdSubstitutions(i, round, _, new)
|
||||
|
|
||||
new order by round
|
||||
)
|
||||
or
|
||||
this.cmdSubstitutionReplacement(result, _, i)
|
||||
}
|
||||
|
||||
private predicate quotedStringReplacement(string quotedStr, string id) {
|
||||
exists(string line, int k | line = this.cmdSubstitutedLineProducer(k) |
|
||||
exists(int i, int j |
|
||||
// double quoted string
|
||||
quotedStr = line.regexpFind("\"((?:[^\"\\\\]|\\\\.)*)\"", i, j) and
|
||||
id =
|
||||
"qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" +
|
||||
quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "")
|
||||
)
|
||||
or
|
||||
exists(int i, int j |
|
||||
// single quoted string
|
||||
quotedStr = line.regexpFind("'((?:\\\\.|[^'\\\\])*)'", i, j) and
|
||||
id =
|
||||
"qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" +
|
||||
quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "")
|
||||
)
|
||||
) and
|
||||
// Only do this for strings that might otherwise disrupt subsequent parsing
|
||||
quotedStr.regexpMatch("[\"'].*[$\n\r'\"" + Bash::separator() + "].*[\"']")
|
||||
}
|
||||
|
||||
private predicate rankedQuotedStringReplacements(int i, string old, string new) {
|
||||
old = rank[i](string old2 | this.quotedStringReplacement(old2, _) | old2) and
|
||||
this.quotedStringReplacement(old, new)
|
||||
}
|
||||
|
||||
private predicate doReplaceQuotedStrings(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.cmdSubstitutedLineProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doReplaceQuotedStrings(line, round - 1, old, middle) and
|
||||
this.rankedQuotedStringReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(target, replacement)
|
||||
)
|
||||
}
|
||||
|
||||
private string quotedStringLineProducer(int i) {
|
||||
result =
|
||||
max(int round, string new | this.doReplaceQuotedStrings(i, round, _, new) | new order by round)
|
||||
}
|
||||
|
||||
private string stmtProducer(int i) {
|
||||
result = this.quotedStringLineProducer(i).splitAt(Bash::splitSeparator()).trim() and
|
||||
// when splitting the line with a separator that is not present, the result is the original line which may contain other separators
|
||||
// we only one the split parts that do not contain any of the separators
|
||||
not result.indexOf(Bash::splitSeparator()) > -1
|
||||
}
|
||||
|
||||
private predicate doStmtRestoreQuotedStrings(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.stmtProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doStmtRestoreQuotedStrings(line, round - 1, old, middle) and
|
||||
this.rankedQuotedStringReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(replacement, target)
|
||||
)
|
||||
}
|
||||
|
||||
private string restoredStmtQuotedStringLineProducer(int i) {
|
||||
result =
|
||||
max(int round, string new |
|
||||
this.doStmtRestoreQuotedStrings(i, round, _, new)
|
||||
|
|
||||
new order by round
|
||||
) and
|
||||
not result.indexOf("qstr:") > -1
|
||||
}
|
||||
|
||||
private predicate doStmtRestoreCmdSubstitutions(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.restoredStmtQuotedStringLineProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doStmtRestoreCmdSubstitutions(line, round - 1, old, middle) and
|
||||
this.rankedCmdSubstitutionReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(replacement, target)
|
||||
)
|
||||
}
|
||||
|
||||
override string getStmt(int i) {
|
||||
result =
|
||||
max(int round, string new |
|
||||
this.doStmtRestoreCmdSubstitutions(i, round, _, new)
|
||||
|
|
||||
new order by round
|
||||
) and
|
||||
not result.indexOf("cmdsubs:") > -1
|
||||
}
|
||||
|
||||
override string getAStmt() { result = this.getStmt(_) }
|
||||
|
||||
private string cmdProducer(int i) {
|
||||
result = this.quotedStringLineProducer(i).splitAt(Bash::separator()).trim() and
|
||||
// when splitting the line with a separator that is not present, the result is the original line which may contain other separators
|
||||
// we only one the split parts that do not contain any of the separators
|
||||
not result.indexOf(Bash::separator()) > -1
|
||||
}
|
||||
|
||||
private predicate doCmdRestoreQuotedStrings(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.cmdProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doCmdRestoreQuotedStrings(line, round - 1, old, middle) and
|
||||
this.rankedQuotedStringReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(replacement, target)
|
||||
)
|
||||
}
|
||||
|
||||
private string restoredCmdQuotedStringLineProducer(int i) {
|
||||
result =
|
||||
max(int round, string new |
|
||||
this.doCmdRestoreQuotedStrings(i, round, _, new)
|
||||
|
|
||||
new order by round
|
||||
) and
|
||||
not result.indexOf("qstr:") > -1
|
||||
}
|
||||
|
||||
private predicate doCmdRestoreCmdSubstitutions(int line, int round, string old, string new) {
|
||||
round = 0 and
|
||||
old = this.restoredCmdQuotedStringLineProducer(line) and
|
||||
new = old
|
||||
or
|
||||
round > 0 and
|
||||
exists(string middle, string target, string replacement |
|
||||
this.doCmdRestoreCmdSubstitutions(line, round - 1, old, middle) and
|
||||
this.rankedCmdSubstitutionReplacements(round, target, replacement) and
|
||||
new = middle.replaceAll(replacement, target)
|
||||
)
|
||||
}
|
||||
|
||||
string getCmd(int i) {
|
||||
result =
|
||||
max(int round, string new |
|
||||
this.doCmdRestoreCmdSubstitutions(i, round, _, new)
|
||||
|
|
||||
new order by round
|
||||
) and
|
||||
not result.indexOf("cmdsubs:") > -1
|
||||
}
|
||||
|
||||
string getACmd() { result = this.getCmd(_) }
|
||||
|
||||
override string getCommand(int i) {
|
||||
// remove redirection
|
||||
result =
|
||||
this.getCmd(i)
|
||||
.regexpReplaceAll("(>|>>|2>|2>>|<|<<<)\\s*[\\{\\}\\$\"'_\\-0-9a-zA-Z]+$", "")
|
||||
.trim() and
|
||||
// exclude variable declarations
|
||||
not result.regexpMatch("^[a-zA-Z0-9\\-_]+=") and
|
||||
// exclude comments
|
||||
not result.trim().indexOf("#") = 0 and
|
||||
// exclude the following keywords
|
||||
not result =
|
||||
[
|
||||
"", "for", "in", "do", "done", "if", "then", "else", "elif", "fi", "while", "until", "case",
|
||||
"esac", "{", "}"
|
||||
]
|
||||
}
|
||||
|
||||
override string getACommand() { result = this.getCommand(_) }
|
||||
|
||||
override string getFileReadCommand(int i) {
|
||||
result = this.getStmt(i) and
|
||||
result.matches(Bash::fileReadCommand() + "%")
|
||||
}
|
||||
|
||||
override string getAFileReadCommand() { result = this.getFileReadCommand(_) }
|
||||
|
||||
override predicate getAssignment(int i, string name, string data) {
|
||||
exists(string stmt |
|
||||
stmt = this.getStmt(i) and
|
||||
name = stmt.regexpCapture("^([a-zA-Z0-9\\-_]+)=.*", 1) and
|
||||
data = stmt.regexpCapture("^[a-zA-Z0-9\\-_]+=(.*)", 1)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate getAnAssignment(string name, string data) { this.getAssignment(_, name, data) }
|
||||
|
||||
override predicate getAWriteToGitHubEnv(string name, string data) {
|
||||
exists(string raw |
|
||||
Bash::extractFileWrite(this, "GITHUB_ENV", raw) and
|
||||
Bash::extractVariableAndValue(raw, name, data)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate getAWriteToGitHubOutput(string name, string data) {
|
||||
exists(string raw |
|
||||
Bash::extractFileWrite(this, "GITHUB_OUTPUT", raw) and
|
||||
Bash::extractVariableAndValue(raw, name, data)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate getAWriteToGitHubPath(string data) {
|
||||
Bash::extractFileWrite(this, "GITHUB_PATH", data)
|
||||
}
|
||||
|
||||
override predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) {
|
||||
Bash::envReachingGitHubFileWrite(this, var, "GITHUB_OUTPUT", output_field)
|
||||
}
|
||||
|
||||
override predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) {
|
||||
Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_OUTPUT", output_field)
|
||||
}
|
||||
|
||||
override predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) {
|
||||
Bash::envReachingGitHubFileWrite(this, var, "GITHUB_ENV", output_field)
|
||||
}
|
||||
|
||||
override predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) {
|
||||
Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_ENV", output_field)
|
||||
}
|
||||
|
||||
override predicate getAnEnvReachingGitHubPathWrite(string var) {
|
||||
Bash::envReachingGitHubFileWrite(this, var, "GITHUB_PATH", _)
|
||||
}
|
||||
|
||||
override predicate getACmdReachingGitHubPathWrite(string cmd) {
|
||||
Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_PATH", _)
|
||||
}
|
||||
|
||||
override predicate getAnEnvReachingArgumentInjectionSink(
|
||||
string var, string command, string argument
|
||||
) {
|
||||
Bash::envReachingArgumentInjectionSink(this, var, command, argument)
|
||||
}
|
||||
|
||||
override predicate getACmdReachingArgumentInjectionSink(
|
||||
string cmd, string command, string argument
|
||||
) {
|
||||
Bash::cmdReachingArgumentInjectionSink(this, cmd, command, argument)
|
||||
}
|
||||
|
||||
override predicate fileToGitHubEnv(string path) {
|
||||
Bash::fileToFileWrite(this, "GITHUB_ENV", path)
|
||||
}
|
||||
|
||||
override predicate fileToGitHubOutput(string path) {
|
||||
Bash::fileToFileWrite(this, "GITHUB_OUTPUT", path)
|
||||
}
|
||||
|
||||
override predicate fileToGitHubPath(string path) {
|
||||
Bash::fileToFileWrite(this, "GITHUB_PATH", path)
|
||||
}
|
||||
}
|
||||
|
||||
module Bash {
|
||||
string stmtSeparator() { result = ";" }
|
||||
|
||||
string commandSeparator() { result = ["&&", "||"] }
|
||||
|
||||
string splitSeparator() {
|
||||
result = stmtSeparator() or
|
||||
result = commandSeparator()
|
||||
}
|
||||
|
||||
string redirectionSeparator() { result = [">", ">>", "2>", "2>>", ">&", "2>&", "<", "<<<"] }
|
||||
|
||||
string pipeSeparator() { result = "|" }
|
||||
|
||||
string separator() {
|
||||
result = stmtSeparator() or
|
||||
result = commandSeparator() or
|
||||
result = pipeSeparator()
|
||||
}
|
||||
|
||||
string fileReadCommand() { result = ["<", "cat", "jq", "yq", "tail", "head"] }
|
||||
|
||||
/** Checks if expr is a bash command substitution */
|
||||
bindingset[expr]
|
||||
predicate isCmdSubstitution(string expr, string cmd) {
|
||||
exists(string regexp |
|
||||
// $(cmd)
|
||||
regexp = "\\$\\(([^)]+)\\)" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
or
|
||||
// `cmd`
|
||||
regexp = "`([^`]+)`" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
)
|
||||
}
|
||||
|
||||
/** Checks if expr is a bash command substitution */
|
||||
bindingset[expr]
|
||||
predicate containsCmdSubstitution(string expr, string cmd) {
|
||||
exists(string regexp |
|
||||
// $(cmd)
|
||||
regexp = ".*\\$\\(([^)]+)\\).*" and
|
||||
cmd = expr.regexpCapture(regexp, 1).trim()
|
||||
or
|
||||
// `cmd`
|
||||
regexp = ".*`([^`]+)`.*" and
|
||||
cmd = expr.regexpCapture(regexp, 1).trim()
|
||||
)
|
||||
}
|
||||
|
||||
/** Checks if expr is a bash parameter expansion */
|
||||
bindingset[expr]
|
||||
predicate isParameterExpansion(string expr, string parameter, string operator, string params) {
|
||||
exists(string regexp |
|
||||
// $VAR
|
||||
regexp = "\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR}
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${!VAR}
|
||||
regexp = "\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 2) and
|
||||
operator = expr.regexpCapture(regexp, 1) and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR<OP><PARAMS>}, ...
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = expr.regexpCapture(regexp, 2) and
|
||||
params = expr.regexpCapture(regexp, 3)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[expr]
|
||||
predicate containsParameterExpansion(string expr, string parameter, string operator, string params) {
|
||||
exists(string regexp |
|
||||
// $VAR
|
||||
regexp = ".*\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR}
|
||||
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${!VAR}
|
||||
regexp = ".*\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 2) and
|
||||
operator = expr.regexpCapture(regexp, 1) and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR<OP><PARAMS>}, ...
|
||||
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = expr.regexpCapture(regexp, 2) and
|
||||
params = expr.regexpCapture(regexp, 3)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[raw_content]
|
||||
predicate extractVariableAndValue(string raw_content, string key, string value) {
|
||||
exists(string regexp, string content | content = trimQuotes(raw_content) |
|
||||
regexp = "(?msi).*^([a-zA-Z_][a-zA-Z0-9_]*)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\2\\s*$" and
|
||||
key = trimQuotes(content.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(content.regexpCapture(regexp, 3))
|
||||
or
|
||||
exists(string line |
|
||||
line = content.splitAt("\n") and
|
||||
regexp = "(?i)^([a-zA-Z_][a-zA-Z0-9_\\-]*)\\s*=\\s*(.*)$" and
|
||||
key = trimQuotes(line.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(line.regexpCapture(regexp, 2))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate singleLineFileWrite(
|
||||
string script, string cmd, string file, string content, string filters
|
||||
) {
|
||||
exists(string regexp |
|
||||
regexp = "(?i)(echo|printf)\\s*(.*?)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = "" and
|
||||
content = script.regexpCapture(regexp, 2)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate singleLineWorkflowCmd(string script, string cmd, string key, string value) {
|
||||
exists(string regexp |
|
||||
regexp = "(?i)(echo|printf)\\s*(['|\"])?::(set-[a-z]+)\\s*name\\s*=\\s*(.*?)::(.*)" and
|
||||
cmd = script.regexpCapture(regexp, 3) and
|
||||
key = script.regexpCapture(regexp, 4) and
|
||||
value = trimQuotes(script.regexpCapture(regexp, 5))
|
||||
or
|
||||
regexp = "(?i)(echo|printf)\\s*(['|\"])?::(add-[a-z]+)\\s*::(.*)" and
|
||||
cmd = script.regexpCapture(regexp, 3) and
|
||||
key = "" and
|
||||
value = trimQuotes(script.regexpCapture(regexp, 4))
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate heredocFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp |
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\4\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 4)) and
|
||||
content = script.regexpCapture(regexp, 6) and
|
||||
filters = ""
|
||||
or
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(<<|<)\\s*[-]?['\"]?(\\S+)['\"]?\\s*([^>]*)(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*\n(.*?)\n\\3\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 7)) and
|
||||
filters = script.regexpCapture(regexp, 4) and
|
||||
content = script.regexpCapture(regexp, 8)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate linesFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp, string var_name |
|
||||
regexp =
|
||||
"(?msi).*((echo|printf)\\s+['|\"]?(.*?<<(\\S+))['|\"]?\\s*>>\\s*(\\S+)\\s*[\r\n]+)" +
|
||||
"(((.*?)\\s*>>\\s*\\S+\\s*[\r\n]+)+)" +
|
||||
"((echo|printf)\\s+['|\"]?(EOF)['|\"]?\\s*>>\\s*\\S+\\s*[\r\n]*).*" and
|
||||
var_name = trimQuotes(script.regexpCapture(regexp, 3)).regexpReplaceAll("<<\\s*(\\S+)", "") and
|
||||
content =
|
||||
var_name + "=$(" +
|
||||
trimQuotes(script.regexpCapture(regexp, 6))
|
||||
.regexpReplaceAll(">>.*GITHUB_(ENV|OUTPUT)(})?", "")
|
||||
.trim() + ")" and
|
||||
cmd = "echo" and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate blockFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp, string first_line, string var_name |
|
||||
regexp =
|
||||
"(?msi).*^\\s*\\{\\s*[\r\n]" +
|
||||
//
|
||||
"(.*?)" +
|
||||
//
|
||||
"(\\s*\\}\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+))\\s*$.*" and
|
||||
first_line = script.regexpCapture(regexp, 1).splitAt("\n", 0).trim() and
|
||||
var_name = first_line.regexpCapture("echo\\s+('|\\\")?(.*)<<.*", 2) and
|
||||
content = var_name + "=$(" + script.regexpCapture(regexp, 1).splitAt("\n").trim() + ")" and
|
||||
not content.indexOf("EOF") > 0 and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
cmd = "echo" and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate multiLineFileWrite(
|
||||
string script, string cmd, string file, string content, string filters
|
||||
) {
|
||||
heredocFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
linesFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
blockFileWrite(script, cmd, file, content, filters)
|
||||
}
|
||||
|
||||
bindingset[file_var]
|
||||
predicate extractFileWrite(BashShellScript script, string file_var, string content) {
|
||||
// single line assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
isParameterExpansion(file_expr, file_var, _, _) and
|
||||
singleLineFileWrite(script.getAStmt(), _, file_expr, raw_content, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
or
|
||||
// workflow command assignment
|
||||
exists(string key, string value, string cmd |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
cmd = "set-env" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
cmd = "set-output" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
cmd = "add-path" and
|
||||
content = value
|
||||
) and
|
||||
singleLineWorkflowCmd(script.getAStmt(), cmd, key, value)
|
||||
)
|
||||
or
|
||||
// multiline assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
multiLineFileWrite(script.getRawScript(), _, file_expr, raw_content, _) and
|
||||
isParameterExpansion(file_expr, file_var, _, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
}
|
||||
|
||||
/** Writes the content of the file specified by `path` into a file pointed to by `file_var` */
|
||||
predicate fileToFileWrite(BashShellScript script, string file_var, string path) {
|
||||
exists(string regexp, string stmt, string file_expr |
|
||||
regexp =
|
||||
"(?i)(cat)\\s*" + "((?:(?!<<|<<-)[^>\n])+)\\s*" +
|
||||
"(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*" + "(\\S+)" and
|
||||
stmt = script.getAStmt() and
|
||||
file_expr = trimQuotes(stmt.regexpCapture(regexp, 5)) and
|
||||
path = stmt.regexpCapture(regexp, 2) and
|
||||
containsParameterExpansion(file_expr, file_var, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the Run scripts contains an access to an environment variable called `var`
|
||||
* which value may get appended to the GITHUB_XXX special file
|
||||
*/
|
||||
predicate envReachingGitHubFileWrite(
|
||||
BashShellScript script, string var, string file_var, string field
|
||||
) {
|
||||
exists(string file_write_value |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
script.getAWriteToGitHubEnv(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
script.getAWriteToGitHubOutput(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
field = "PATH" and
|
||||
script.getAWriteToGitHubPath(file_write_value)
|
||||
) and
|
||||
envReachingRunExpr(script, var, file_write_value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if and environment variable is used, directly or indirectly, in a Run's step expression.
|
||||
* Where the expression is a string captured from the Run's script.
|
||||
*/
|
||||
bindingset[expr]
|
||||
predicate envReachingRunExpr(BashShellScript script, string var, string expr) {
|
||||
exists(string var2, string value2 |
|
||||
// VAR2=${VAR:-default} (var2=value2)
|
||||
// echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value)
|
||||
script.getAnAssignment(var2, value2) and
|
||||
containsParameterExpansion(value2, var, _, _) and
|
||||
containsParameterExpansion(expr, var2, _, _)
|
||||
)
|
||||
or
|
||||
// var reaches the file write directly
|
||||
// echo "FIELD=${VAR:-default}" >> $GITHUB_ENV (field, file_write_value)
|
||||
containsParameterExpansion(expr, var, _, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the Run scripts contains a command substitution (`cmd`)
|
||||
* which output may get appended to the GITHUB_XXX special file
|
||||
*/
|
||||
predicate cmdReachingGitHubFileWrite(
|
||||
BashShellScript script, string cmd, string file_var, string field
|
||||
) {
|
||||
exists(string file_write_value |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
script.getAWriteToGitHubEnv(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
script.getAWriteToGitHubOutput(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
field = "PATH" and
|
||||
script.getAWriteToGitHubPath(file_write_value)
|
||||
) and
|
||||
cmdReachingRunExpr(script, cmd, file_write_value)
|
||||
)
|
||||
}
|
||||
|
||||
predicate envReachingArgumentInjectionSink(
|
||||
BashShellScript script, string source, string command, string argument
|
||||
) {
|
||||
exists(string cmd, string regex, int command_group, int argument_group |
|
||||
cmd = script.getACommand() and
|
||||
argumentInjectionSinksDataModel(regex, command_group, argument_group) and
|
||||
argument = cmd.regexpCapture(regex, argument_group).trim() and
|
||||
command = cmd.regexpCapture(regex, command_group).trim() and
|
||||
envReachingRunExpr(script, source, argument)
|
||||
)
|
||||
}
|
||||
|
||||
predicate cmdReachingArgumentInjectionSink(
|
||||
BashShellScript script, string source, string command, string argument
|
||||
) {
|
||||
exists(string cmd, string regex, int command_group, int argument_group |
|
||||
cmd = script.getACommand() and
|
||||
argumentInjectionSinksDataModel(regex, command_group, argument_group) and
|
||||
argument = cmd.regexpCapture(regex, argument_group).trim() and
|
||||
command = cmd.regexpCapture(regex, command_group).trim() and
|
||||
cmdReachingRunExpr(script, source, argument)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a command output is used, directly or indirectly, in a Run's step expression.
|
||||
* Where the expression is a string captured from the Run's script.
|
||||
*/
|
||||
bindingset[expr]
|
||||
predicate cmdReachingRunExpr(BashShellScript script, string cmd, string expr) {
|
||||
// cmd output is assigned to a second variable (var2) and var2 reaches the file write
|
||||
exists(string var2, string value2 |
|
||||
// VAR2=$(cmd)
|
||||
// echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value)
|
||||
script.getAnAssignment(var2, value2) and
|
||||
containsCmdSubstitution(value2, cmd) and
|
||||
containsParameterExpansion(expr, var2, _, _) and
|
||||
not varMatchesRegexTest(script, var2, alphaNumericRegex())
|
||||
)
|
||||
or
|
||||
exists(string var2, string value2, string var3, string value3 |
|
||||
// VAR2=$(cmd)
|
||||
// VAR3=$VAR2
|
||||
// echo "FIELD=${VAR3:-default}" >> $GITHUB_ENV (field, file_write_value)
|
||||
containsCmdSubstitution(value2, cmd) and
|
||||
script.getAnAssignment(var2, value2) and
|
||||
containsParameterExpansion(value3, var2, _, _) and
|
||||
script.getAnAssignment(var3, value3) and
|
||||
containsParameterExpansion(expr, var3, _, _) and
|
||||
not varMatchesRegexTest(script, var2, alphaNumericRegex()) and
|
||||
not varMatchesRegexTest(script, var3, alphaNumericRegex())
|
||||
)
|
||||
or
|
||||
// var reaches the file write directly
|
||||
// echo "FIELD=$(cmd)" >> $GITHUB_ENV (field, file_write_value)
|
||||
containsCmdSubstitution(expr, cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there test command that checks a variable against a regex
|
||||
* eg: `[[ $VAR =~ ^[a-zA-Z0-9_]+$ ]]`
|
||||
*/
|
||||
bindingset[var, regex]
|
||||
predicate varMatchesRegexTest(BashShellScript script, string var, string regex) {
|
||||
exists(string lhs, string rhs |
|
||||
lhs = script.getACommand().regexpCapture(".*\\[\\[\\s*(.*?)\\s*=~\\s*(.*?)\\s*\\]\\].*", 1) and
|
||||
containsParameterExpansion(lhs, var, _, _) and
|
||||
rhs = script.getACommand().regexpCapture(".*\\[\\[\\s*(.*?)\\s*=~\\s*(.*?)\\s*\\]\\].*", 2) and
|
||||
trimQuotes(rhs).regexpMatch(regex)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the given regex is used to match an alphanumeric string
|
||||
* eg: `^[0-9a-zA-Z]{40}$`, `^[0-9]+$` or `^[a-zA-Z0-9_]+$`
|
||||
*/
|
||||
string alphaNumericRegex() { result = "^\\^\\[([09azAZ_-]+)\\](\\+|\\{\\d+\\})\\$$" }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/** Provides classes representing the control flow graph. */
|
||||
|
||||
private import codeql.actions.controlflow.internal.Cfg as CfgInternal
|
||||
import CfgInternal::Completion
|
||||
import CfgInternal::CfgScope
|
||||
import CfgInternal::CfgImpl
|
||||
@@ -1 +0,0 @@
|
||||
import DataFlow::DataFlow::Consistency
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Provides classes for performing local (intra-procedural) and
|
||||
* global (inter-procedural) data flow analyses.
|
||||
*/
|
||||
|
||||
import codeql.Locations
|
||||
|
||||
module DataFlow {
|
||||
private import codeql.dataflow.DataFlow
|
||||
private import codeql.actions.dataflow.internal.DataFlowImplSpecific
|
||||
import DataFlowMake<Location, ActionsDataFlow>
|
||||
import codeql.actions.dataflow.internal.DataFlowPublic
|
||||
// debug
|
||||
private import codeql.actions.dataflow.internal.TaintTrackingImplSpecific
|
||||
import codeql.dataflow.internal.DataFlowImplConsistency as DFIC
|
||||
|
||||
module ActionsConsistency implements DFIC::InputSig<Location, ActionsDataFlow> { }
|
||||
|
||||
module Consistency {
|
||||
import DFIC::MakeConsistency<Location, ActionsDataFlow, ActionsTaintTracking, ActionsConsistency>
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.Locations
|
||||
private import codeql.actions.security.ControlChecks
|
||||
import codeql.actions.config.Config
|
||||
import codeql.actions.Bash
|
||||
import codeql.actions.PowerShell
|
||||
|
||||
bindingset[expr]
|
||||
string normalizeExpr(string expr) {
|
||||
result =
|
||||
expr.regexpReplaceAll("\\['([a-zA-Z0-9_\\*\\-]+)'\\]", ".$1")
|
||||
.regexpReplaceAll("\\[\"([a-zA-Z0-9_\\*\\-]+)\"\\]", ".$1")
|
||||
.regexpReplaceAll("\\s*\\.\\s*", ".")
|
||||
}
|
||||
|
||||
bindingset[regex]
|
||||
string wrapRegexp(string regex) { result = "\\b" + regex + "\\b" }
|
||||
|
||||
bindingset[regex]
|
||||
string wrapJsonRegexp(string regex) {
|
||||
result = ["fromJSON\\(\\s*" + regex + "\\s*\\)", "toJSON\\(\\s*" + regex + "\\s*\\)"]
|
||||
}
|
||||
|
||||
bindingset[str]
|
||||
string trimQuotes(string str) {
|
||||
result = str.trim().regexpReplaceAll("^(\"|')", "").regexpReplaceAll("(\"|')$", "")
|
||||
}
|
||||
|
||||
predicate inPrivilegedContext(AstNode node, Event event) {
|
||||
node.getEnclosingJob().isPrivilegedExternallyTriggerable(event)
|
||||
}
|
||||
|
||||
predicate inNonPrivilegedContext(AstNode node) {
|
||||
not node.getEnclosingJob().isPrivilegedExternallyTriggerable(_)
|
||||
}
|
||||
|
||||
string defaultBranchNames() {
|
||||
repositoryDataModel(_, result)
|
||||
or
|
||||
not exists(string default_branch_name | repositoryDataModel(_, default_branch_name)) and
|
||||
result = ["main", "master"]
|
||||
}
|
||||
|
||||
string getRepoRoot() {
|
||||
exists(Workflow w |
|
||||
w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
|
||||
result =
|
||||
w.getLocation()
|
||||
.getFile()
|
||||
.getRelativePath()
|
||||
.prefix(w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") + 1) and
|
||||
// exclude workflow_enum reusable workflows directory root
|
||||
not result.indexOf(".github/workflows/external/") > -1 and
|
||||
not result.indexOf(".github/actions/external/") > -1
|
||||
or
|
||||
not w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
|
||||
not w.getLocation().getFile().getRelativePath().indexOf(".github/workflows/external/") > -1 and
|
||||
not w.getLocation().getFile().getRelativePath().indexOf(".github/actions/external/") > -1 and
|
||||
result = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[path]
|
||||
string normalizePath(string path) {
|
||||
exists(string trimmed_path | trimmed_path = trimQuotes(path) |
|
||||
// ./foo -> GITHUB_WORKSPACE/foo
|
||||
if path.indexOf("./") = 0
|
||||
then result = path.replaceAll("./", "GITHUB_WORKSPACE/")
|
||||
else
|
||||
// GITHUB_WORKSPACE/foo -> GITHUB_WORKSPACE/foo
|
||||
if path.indexOf("GITHUB_WORKSPACE/") = 0
|
||||
then result = path
|
||||
else
|
||||
// foo -> GITHUB_WORKSPACE/foo
|
||||
if path.regexpMatch("^[^/~].*")
|
||||
then result = "GITHUB_WORKSPACE/" + path.regexpReplaceAll("/$", "")
|
||||
else
|
||||
// ~/foo -> ~/foo
|
||||
// /foo -> /foo
|
||||
result = path
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the path cache_path is a subpath of the path untrusted_path.
|
||||
*/
|
||||
bindingset[subpath, path]
|
||||
predicate isSubpath(string subpath, string path) { subpath.substring(0, path.length()) = path }
|
||||
@@ -1,62 +0,0 @@
|
||||
private import codeql.actions.Ast
|
||||
|
||||
class PowerShellScript extends ShellScript {
|
||||
PowerShellScript() {
|
||||
exists(Run run |
|
||||
this = run.getScript() and
|
||||
run.getShell().matches("pwsh%")
|
||||
)
|
||||
}
|
||||
|
||||
override string getStmt(int i) { none() }
|
||||
|
||||
override string getAStmt() { none() }
|
||||
|
||||
override string getCommand(int i) { none() }
|
||||
|
||||
override string getACommand() { none() }
|
||||
|
||||
override string getFileReadCommand(int i) { none() }
|
||||
|
||||
override string getAFileReadCommand() { none() }
|
||||
|
||||
override predicate getAssignment(int i, string name, string data) { none() }
|
||||
|
||||
override predicate getAnAssignment(string name, string data) { none() }
|
||||
|
||||
override predicate getAWriteToGitHubEnv(string name, string data) { none() }
|
||||
|
||||
override predicate getAWriteToGitHubOutput(string name, string data) { none() }
|
||||
|
||||
override predicate getAWriteToGitHubPath(string data) { none() }
|
||||
|
||||
override predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) { none() }
|
||||
|
||||
override predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) { none() }
|
||||
|
||||
override predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) { none() }
|
||||
|
||||
override predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) { none() }
|
||||
|
||||
override predicate getAnEnvReachingGitHubPathWrite(string var) { none() }
|
||||
|
||||
override predicate getACmdReachingGitHubPathWrite(string cmd) { none() }
|
||||
|
||||
override predicate getAnEnvReachingArgumentInjectionSink(
|
||||
string var, string command, string argument
|
||||
) {
|
||||
none()
|
||||
}
|
||||
|
||||
override predicate getACmdReachingArgumentInjectionSink(
|
||||
string cmd, string command, string argument
|
||||
) {
|
||||
none()
|
||||
}
|
||||
|
||||
override predicate fileToGitHubEnv(string path) { none() }
|
||||
|
||||
override predicate fileToGitHubOutput(string path) { none() }
|
||||
|
||||
override predicate fileToGitHubPath(string path) { none() }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Provides classes for performing local (intra-procedural) and
|
||||
* global (inter-procedural) taint-tracking analyses.
|
||||
*/
|
||||
|
||||
import codeql.Locations
|
||||
|
||||
module TaintTracking {
|
||||
private import codeql.actions.dataflow.internal.DataFlowImplSpecific
|
||||
private import codeql.actions.dataflow.internal.TaintTrackingImplSpecific
|
||||
private import codeql.dataflow.TaintTracking
|
||||
import TaintFlowMake<Location, ActionsDataFlow, ActionsTaintTracking>
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
private import actions
|
||||
|
||||
/**
|
||||
* Holds if workflow step uses the github/codeql-action/init action with no customizations.
|
||||
* e.g.
|
||||
* - name: Initialize
|
||||
* uses: github/codeql-action/init@v2
|
||||
* with:
|
||||
* languages: ruby, javascript
|
||||
*/
|
||||
class DefaultableCodeQLInitiatlizeActionQuery extends UsesStep {
|
||||
DefaultableCodeQLInitiatlizeActionQuery() {
|
||||
this.getCallee() = "github/codeql-action/init" and
|
||||
not customizedWorkflowStep(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the with: part of the workflow step contains any arguments for with: other than "languages".
|
||||
* e.g.
|
||||
* - name: Initialize CodeQL
|
||||
* uses: github/codeql-action/init@v3
|
||||
* with:
|
||||
* languages: ${{ matrix.language }}
|
||||
* config-file: ./.github/codeql/${{ matrix.language }}/codeql-config.yml
|
||||
*/
|
||||
predicate customizedWorkflowStep(UsesStep codeQLInitStep) {
|
||||
exists(string arg |
|
||||
exists(codeQLInitStep.getArgument(arg)) and
|
||||
arg != "languages"
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Provides classes for working with YAML data.
|
||||
*
|
||||
* YAML documents are represented as abstract syntax trees whose nodes
|
||||
* are either YAML values or alias nodes referring to another YAML value.
|
||||
*/
|
||||
|
||||
private import codeql.yaml.Yaml as LibYaml
|
||||
|
||||
private module YamlSig implements LibYaml::InputSig {
|
||||
import codeql.Locations
|
||||
|
||||
class LocatableBase extends @yaml_locatable {
|
||||
Location getLocation() {
|
||||
exists(@location_default loc, File f, string p, int sl, int sc, int el, int ec |
|
||||
f.getAbsolutePath() = p and
|
||||
locations_default(loc, f, sl, sc, el, ec) and
|
||||
yaml_locations(this, loc) and
|
||||
result = TBaseLocation(p, sl, sc, el, ec)
|
||||
)
|
||||
}
|
||||
|
||||
string toString() { none() }
|
||||
}
|
||||
|
||||
class NodeBase extends LocatableBase, @yaml_node {
|
||||
NodeBase getChildNode(int i) { yaml(result, _, this, i, _, _) }
|
||||
|
||||
string getTag() { yaml(this, _, _, _, result, _) }
|
||||
|
||||
string getAnchor() { yaml_anchors(this, result) }
|
||||
|
||||
override string toString() { yaml(this, _, _, _, _, result) }
|
||||
}
|
||||
|
||||
class ScalarNodeBase extends NodeBase, @yaml_scalar_node {
|
||||
int getStyle() { yaml_scalars(this, result, _) }
|
||||
|
||||
string getValue() { yaml_scalars(this, _, result) }
|
||||
}
|
||||
|
||||
class CollectionNodeBase extends NodeBase, @yaml_collection_node { }
|
||||
|
||||
class MappingNodeBase extends CollectionNodeBase, @yaml_mapping_node { }
|
||||
|
||||
class SequenceNodeBase extends CollectionNodeBase, @yaml_sequence_node { }
|
||||
|
||||
class AliasNodeBase extends NodeBase, @yaml_alias_node {
|
||||
string getTarget() { yaml_aliases(this, result) }
|
||||
}
|
||||
|
||||
class ParseErrorBase extends LocatableBase, @yaml_error {
|
||||
string getMessage() { yaml_errors(this, result) }
|
||||
}
|
||||
}
|
||||
|
||||
import LibYaml::Make<YamlSig>
|
||||
@@ -1,156 +0,0 @@
|
||||
import ConfigExtensions as Extensions
|
||||
|
||||
/**
|
||||
* MaD models for workflow details
|
||||
* Fields:
|
||||
* - path: Path to the workflow file
|
||||
* - trigger: Trigger for the workflow
|
||||
* - job: Job name
|
||||
* - secrets_source: Source of secrets
|
||||
* - permissions: Permissions for the workflow
|
||||
* - runner: Runner info for the workflow
|
||||
*/
|
||||
predicate workflowDataModel(
|
||||
string path, string trigger, string job, string secrets_source, string permissions, string runner
|
||||
) {
|
||||
Extensions::workflowDataModel(path, trigger, job, secrets_source, permissions, runner)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for repository details
|
||||
* Fields:
|
||||
* - visibility: Visibility of the repository
|
||||
* - default_branch_name: Default branch name
|
||||
*/
|
||||
predicate repositoryDataModel(string visibility, string default_branch_name) {
|
||||
Extensions::repositoryDataModel(visibility, default_branch_name)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for context/trigger mapping
|
||||
* Fields:
|
||||
* - trigger: Trigger for the workflow
|
||||
* - context_prefix: Prefix for the context
|
||||
*/
|
||||
predicate contextTriggerDataModel(string trigger, string context_prefix) {
|
||||
Extensions::contextTriggerDataModel(trigger, context_prefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for externally triggerable events
|
||||
* Fields:
|
||||
* - event: Event name
|
||||
*/
|
||||
predicate externallyTriggerableEventsDataModel(string event) {
|
||||
Extensions::externallyTriggerableEventsDataModel(event)
|
||||
}
|
||||
|
||||
private string commandLauncher() { result = ["", "sudo\\s+", "su\\s+", "xvfb-run\\s+"] }
|
||||
|
||||
/**
|
||||
* MaD models for poisonable commands
|
||||
* Fields:
|
||||
* - regexp: Regular expression for matching poisonable commands
|
||||
*/
|
||||
predicate poisonableCommandsDataModel(string regexp) {
|
||||
exists(string sub_regexp |
|
||||
Extensions::poisonableCommandsDataModel(sub_regexp) and
|
||||
regexp = commandLauncher() + sub_regexp + ".*"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for poisonable local scripts
|
||||
* Fields:
|
||||
* - regexp: Regular expression for matching poisonable local scripts
|
||||
* - group: Script capture group number for the regular expression
|
||||
*/
|
||||
predicate poisonableLocalScriptsDataModel(string regexp, int command_group) {
|
||||
exists(string sub_regexp |
|
||||
Extensions::poisonableLocalScriptsDataModel(sub_regexp, command_group) and
|
||||
regexp = commandLauncher() + sub_regexp + ".*"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for arguments to commands that execute the given argument.
|
||||
* Fields:
|
||||
* - regexp: Regular expression for matching argument injections.
|
||||
* - command_group: capture group for the command.
|
||||
* - argument_group: capture group for the argument.
|
||||
*/
|
||||
predicate argumentInjectionSinksDataModel(string regexp, int command_group, int argument_group) {
|
||||
exists(string sub_regexp |
|
||||
Extensions::argumentInjectionSinksDataModel(sub_regexp, command_group, argument_group) and
|
||||
regexp = commandLauncher() + sub_regexp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for poisonable actions
|
||||
* Fields:
|
||||
* - action: action name
|
||||
*/
|
||||
predicate poisonableActionsDataModel(string action) {
|
||||
Extensions::poisonableActionsDataModel(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for event properties that can be user-controlled.
|
||||
* Fields:
|
||||
* - property: event property
|
||||
* - kind: property kind
|
||||
*/
|
||||
predicate untrustedEventPropertiesDataModel(string property, string kind) {
|
||||
Extensions::untrustedEventPropertiesDataModel(property, kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for vulnerable actions
|
||||
* Fields:
|
||||
* - action: action name
|
||||
* - vulnerable_version: vulnerable version
|
||||
* - vulnerable_sha: vulnerable sha
|
||||
* - fixed_version: fixed version
|
||||
*/
|
||||
predicate vulnerableActionsDataModel(
|
||||
string action, string vulnerable_version, string vulnerable_sha, string fixed_version
|
||||
) {
|
||||
Extensions::vulnerableActionsDataModel(action, vulnerable_version, vulnerable_sha, fixed_version)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for immutable actions
|
||||
* Fields:
|
||||
* - action: action name
|
||||
*/
|
||||
predicate immutableActionsDataModel(string action) { Extensions::immutableActionsDataModel(action) }
|
||||
|
||||
/**
|
||||
* MaD models for trusted actions owners
|
||||
* Fields:
|
||||
* - owner: owner name
|
||||
*/
|
||||
predicate trustedActionsOwnerDataModel(string owner) {
|
||||
Extensions::trustedActionsOwnerDataModel(owner)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for untrusted git commands
|
||||
* Fields:
|
||||
* - cmd_regex: Regular expression for matching untrusted git commands
|
||||
* - flag: Flag for the command
|
||||
*/
|
||||
predicate untrustedGitCommandDataModel(string cmd_regex, string flag) {
|
||||
Extensions::untrustedGitCommandDataModel(cmd_regex, flag)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD models for untrusted gh commands
|
||||
* Fields:
|
||||
* - cmd_regex: Regular expression for matching untrusted gh commands
|
||||
* - flag: Flag for the command
|
||||
*/
|
||||
predicate untrustedGhCommandDataModel(string cmd_regex, string flag) {
|
||||
Extensions::untrustedGhCommandDataModel(cmd_regex, flag)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* This module provides extensible predicates for defining MaD models.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds if workflow data model exists for the given parameters.
|
||||
*/
|
||||
extensible predicate workflowDataModel(
|
||||
string path, string trigger, string job, string secrets_source, string permissions, string runner
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds if repository data model exists for the given parameters.
|
||||
*/
|
||||
extensible predicate repositoryDataModel(string visibility, string default_branch_name);
|
||||
|
||||
/**
|
||||
* Holds if a context expression starting with context_prefix is available for a given trigger.
|
||||
*/
|
||||
extensible predicate contextTriggerDataModel(string trigger, string context_prefix);
|
||||
|
||||
/**
|
||||
* Holds if a given trigger event can be fired by an external actor.
|
||||
*/
|
||||
extensible predicate externallyTriggerableEventsDataModel(string event);
|
||||
|
||||
/**
|
||||
* Holds for strings that match poisonable commands.
|
||||
*/
|
||||
extensible predicate poisonableCommandsDataModel(string regexp);
|
||||
|
||||
/**
|
||||
* Holds for strings that match poisonable local scripts.
|
||||
*/
|
||||
extensible predicate poisonableLocalScriptsDataModel(string regexp, int group);
|
||||
|
||||
/**
|
||||
* Holds for actions that can be poisoned through local files.
|
||||
*/
|
||||
extensible predicate poisonableActionsDataModel(string action);
|
||||
|
||||
/**
|
||||
* Holds for event properties that can be user-controlled.
|
||||
*/
|
||||
extensible predicate untrustedEventPropertiesDataModel(string property, string kind);
|
||||
|
||||
/**
|
||||
* Holds for arguments to commands that execute the given argument
|
||||
*/
|
||||
extensible predicate argumentInjectionSinksDataModel(
|
||||
string regexp, int command_group, int argument_group
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds for actions that are known to be vulnerable.
|
||||
*/
|
||||
extensible predicate vulnerableActionsDataModel(
|
||||
string action, string vulnerable_version, string vulnerable_sha, string fixed_version
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds for actions that are known to be immutable.
|
||||
*/
|
||||
extensible predicate immutableActionsDataModel(string action);
|
||||
|
||||
/**
|
||||
* Holds for trusted Actions owners.
|
||||
*/
|
||||
extensible predicate trustedActionsOwnerDataModel(string owner);
|
||||
|
||||
/**
|
||||
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
|
||||
*/
|
||||
extensible predicate untrustedGitCommandDataModel(string cmd_regex, string flag);
|
||||
|
||||
/**
|
||||
* Holds for gh commands that may introduce untrusted data
|
||||
*/
|
||||
extensible predicate untrustedGhCommandDataModel(string cmd_regex, string flag);
|
||||
@@ -1,444 +0,0 @@
|
||||
/** Provides classes representing basic blocks. */
|
||||
|
||||
private import codeql.actions.Cfg
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.Locations
|
||||
|
||||
/**
|
||||
* A basic block, that is, a maximal straight-line sequence of control flow nodes
|
||||
* without branches or joins.
|
||||
*/
|
||||
class BasicBlock extends TBasicBlockStart {
|
||||
/** Gets the scope of this basic block. */
|
||||
final CfgScope getScope() { result = this.getFirstNode().getScope() }
|
||||
|
||||
/** Gets an immediate successor of this basic block, if any. */
|
||||
BasicBlock getASuccessor() { result = this.getASuccessor(_) }
|
||||
|
||||
/** Gets an immediate successor of this basic block of a given type, if any. */
|
||||
BasicBlock getASuccessor(SuccessorType t) {
|
||||
result.getFirstNode() = this.getLastNode().getASuccessor(t)
|
||||
}
|
||||
|
||||
/** Gets an immediate predecessor of this basic block, if any. */
|
||||
BasicBlock getAPredecessor() { result.getASuccessor() = this }
|
||||
|
||||
/** Gets an immediate predecessor of this basic block of a given type, if any. */
|
||||
BasicBlock getAPredecessor(SuccessorType t) { result.getASuccessor(t) = this }
|
||||
|
||||
/** Gets the control flow node at a specific (zero-indexed) position in this basic block. */
|
||||
Node getNode(int pos) { bbIndex(this.getFirstNode(), result, pos) }
|
||||
|
||||
/** Gets a control flow node in this basic block. */
|
||||
Node getANode() { result = this.getNode(_) }
|
||||
|
||||
/** Gets the first control flow node in this basic block. */
|
||||
Node getFirstNode() { this = TBasicBlockStart(result) }
|
||||
|
||||
/** Gets the last control flow node in this basic block. */
|
||||
Node getLastNode() { result = this.getNode(this.length() - 1) }
|
||||
|
||||
/** Gets the length of this basic block. */
|
||||
int length() { result = strictcount(this.getANode()) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block immediately dominates basic block `bb`.
|
||||
*
|
||||
* That is, all paths reaching basic block `bb` from some entry point
|
||||
* basic block must go through this basic block (which is an immediate
|
||||
* predecessor of `bb`).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* return 0
|
||||
* end
|
||||
* return 1
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block starting on line 2 immediately dominates the
|
||||
* basic block on line 5 (all paths from the entry point of `m`
|
||||
* to `return 1` must go through the `if` block).
|
||||
*/
|
||||
predicate immediatelyDominates(BasicBlock bb) { bbIDominates(this, bb) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block strictly dominates basic block `bb`.
|
||||
*
|
||||
* That is, all paths reaching basic block `bb` from some entry point
|
||||
* basic block must go through this basic block (which must be different
|
||||
* from `bb`).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* return 0
|
||||
* end
|
||||
* return 1
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block starting on line 2 strictly dominates the
|
||||
* basic block on line 5 (all paths from the entry point of `m`
|
||||
* to `return 1` must go through the `if` block).
|
||||
*/
|
||||
predicate strictlyDominates(BasicBlock bb) { bbIDominates+(this, bb) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block dominates basic block `bb`.
|
||||
*
|
||||
* That is, all paths reaching basic block `bb` from some entry point
|
||||
* basic block must go through this basic block.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* return 0
|
||||
* end
|
||||
* return 1
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block starting on line 2 dominates the basic
|
||||
* basic block on line 5 (all paths from the entry point of `m`
|
||||
* to `return 1` must go through the `if` block).
|
||||
*/
|
||||
predicate dominates(BasicBlock bb) {
|
||||
bb = this or
|
||||
this.strictlyDominates(bb)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `df` is in the dominance frontier of this basic block.
|
||||
* That is, this basic block dominates a predecessor of `df`, but
|
||||
* does not dominate `df` itself.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m x
|
||||
* if x < 0
|
||||
* x = -x
|
||||
* if x > 10
|
||||
* x = x - 1
|
||||
* end
|
||||
* end
|
||||
* puts x
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block on line 8 is in the dominance frontier
|
||||
* of the basic block starting on line 3 because that block
|
||||
* dominates the basic block on line 4, which is a predecessor of
|
||||
* `puts x`. Also, the basic block starting on line 3 does not
|
||||
* dominate the basic block on line 8.
|
||||
*/
|
||||
predicate inDominanceFrontier(BasicBlock df) {
|
||||
this.dominatesPredecessor(df) and
|
||||
not this.strictlyDominates(df)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this basic block dominates a predecessor of `df`.
|
||||
*/
|
||||
private predicate dominatesPredecessor(BasicBlock df) { this.dominates(df.getAPredecessor()) }
|
||||
|
||||
/**
|
||||
* Gets the basic block that immediately dominates this basic block, if any.
|
||||
*
|
||||
* That is, all paths reaching this basic block from some entry point
|
||||
* basic block must go through the result, which is an immediate basic block
|
||||
* predecessor of this basic block.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* return 0
|
||||
* end
|
||||
* return 1
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block starting on line 2 is an immediate dominator of
|
||||
* the basic block on line 5 (all paths from the entry point of `m`
|
||||
* to `return 1` must go through the `if` block, and the `if` block
|
||||
* is an immediate predecessor of `return 1`).
|
||||
*/
|
||||
BasicBlock getImmediateDominator() { bbIDominates(result, this) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block strictly post-dominates basic block `bb`.
|
||||
*
|
||||
* That is, all paths reaching a normal exit point basic block from basic
|
||||
* block `bb` must go through this basic block (which must be different
|
||||
* from `bb`).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* puts "b"
|
||||
* end
|
||||
* puts "m"
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block on line 5 strictly post-dominates the basic block on
|
||||
* line 3 (all paths to the exit point of `m` from `puts "b"` must go
|
||||
* through `puts "m"`).
|
||||
*/
|
||||
predicate strictlyPostDominates(BasicBlock bb) { bbIPostDominates+(this, bb) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block post-dominates basic block `bb`.
|
||||
*
|
||||
* That is, all paths reaching a normal exit point basic block from basic
|
||||
* block `bb` must go through this basic block.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```rb
|
||||
* def m b
|
||||
* if b
|
||||
* puts "b"
|
||||
* end
|
||||
* puts "m"
|
||||
* end
|
||||
* ```
|
||||
*
|
||||
* The basic block on line 5 post-dominates the basic block on line 3
|
||||
* (all paths to the exit point of `m` from `puts "b"` must go through
|
||||
* `puts "m"`).
|
||||
*/
|
||||
predicate postDominates(BasicBlock bb) {
|
||||
this.strictlyPostDominates(bb) or
|
||||
this = bb
|
||||
}
|
||||
|
||||
/** Holds if this basic block is in a loop in the control flow graph. */
|
||||
predicate inLoop() { this.getASuccessor+() = this }
|
||||
|
||||
/** Gets a textual representation of this basic block. */
|
||||
string toString() { result = this.getFirstNode().toString() }
|
||||
|
||||
/** Gets the location of this basic block. */
|
||||
Location getLocation() { result = this.getFirstNode().getLocation() }
|
||||
}
|
||||
|
||||
cached
|
||||
private module Cached {
|
||||
/** Internal representation of basic blocks. */
|
||||
cached
|
||||
newtype TBasicBlock = TBasicBlockStart(Node cfn) { startsBB(cfn) }
|
||||
|
||||
/** Holds if `cfn` starts a new basic block. */
|
||||
private predicate startsBB(Node cfn) {
|
||||
not exists(cfn.getAPredecessor()) and exists(cfn.getASuccessor())
|
||||
or
|
||||
cfn.isJoin()
|
||||
or
|
||||
cfn.getAPredecessor().isBranch()
|
||||
or
|
||||
/*
|
||||
* In cases such as
|
||||
*
|
||||
* ```rb
|
||||
* if x or y
|
||||
* foo
|
||||
* else
|
||||
* bar
|
||||
* ```
|
||||
*
|
||||
* we have a CFG that looks like
|
||||
*
|
||||
* x --false--> [false] x or y --false--> bar
|
||||
* \ |
|
||||
* --true--> y --false--
|
||||
* \
|
||||
* --true--> [true] x or y --true--> foo
|
||||
*
|
||||
* and we want to ensure that both `foo` and `bar` start a new basic block,
|
||||
* in order to get a `ConditionalBlock` out of the disjunction.
|
||||
*/
|
||||
|
||||
exists(cfn.getAPredecessor(any(BooleanSuccessor s)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `succ` is a control flow successor of `pred` within
|
||||
* the same basic block.
|
||||
*/
|
||||
private predicate intraBBSucc(Node pred, Node succ) {
|
||||
succ = pred.getASuccessor() and
|
||||
not startsBB(succ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `cfn` is the `i`th node in basic block `bb`.
|
||||
*
|
||||
* In other words, `i` is the shortest distance from a node `bb`
|
||||
* that starts a basic block to `cfn` along the `intraBBSucc` relation.
|
||||
*/
|
||||
cached
|
||||
predicate bbIndex(Node bbStart, Node cfn, int i) =
|
||||
shortestDistances(startsBB/1, intraBBSucc/2)(bbStart, cfn, i)
|
||||
|
||||
/**
|
||||
* Holds if the first node of basic block `succ` is a control flow
|
||||
* successor of the last node of basic block `pred`.
|
||||
*/
|
||||
private predicate succBB(BasicBlock pred, BasicBlock succ) { succ = pred.getASuccessor() }
|
||||
|
||||
/** Holds if `dom` is an immediate dominator of `bb`. */
|
||||
cached
|
||||
predicate bbIDominates(BasicBlock dom, BasicBlock bb) =
|
||||
idominance(entryBB/1, succBB/2)(_, dom, bb)
|
||||
|
||||
/** Holds if `pred` is a basic block predecessor of `succ`. */
|
||||
private predicate predBB(BasicBlock succ, BasicBlock pred) { succBB(pred, succ) }
|
||||
|
||||
/** Holds if `bb` is an exit basic block that represents normal exit. */
|
||||
private predicate normalExitBB(BasicBlock bb) { bb.getANode().(AnnotatedExitNode).isNormal() }
|
||||
|
||||
/** Holds if `dom` is an immediate post-dominator of `bb`. */
|
||||
cached
|
||||
predicate bbIPostDominates(BasicBlock dom, BasicBlock bb) =
|
||||
idominance(normalExitBB/1, predBB/2)(_, dom, bb)
|
||||
|
||||
/**
|
||||
* Gets the `i`th predecessor of join block `jb`, with respect to some
|
||||
* arbitrary order.
|
||||
*/
|
||||
cached
|
||||
JoinBlockPredecessor getJoinBlockPredecessor(JoinBlock jb, int i) {
|
||||
none()
|
||||
/*
|
||||
* result =
|
||||
* rank[i + 1](JoinBlockPredecessor jbp |
|
||||
* jbp = jb.getAPredecessor()
|
||||
* |
|
||||
* jbp order by JoinBlockPredecessors::getId(jbp), JoinBlockPredecessors::getSplitString(jbp)
|
||||
* )
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
cached
|
||||
predicate immediatelyControls(ConditionBlock cb, BasicBlock succ, BooleanSuccessor s) {
|
||||
succ = cb.getASuccessor(s) and
|
||||
forall(BasicBlock pred | pred = succ.getAPredecessor() and pred != cb | succ.dominates(pred))
|
||||
}
|
||||
|
||||
cached
|
||||
predicate controls(ConditionBlock cb, BasicBlock controlled, BooleanSuccessor s) {
|
||||
exists(BasicBlock succ | cb.immediatelyControls(succ, s) | succ.dominates(controlled))
|
||||
}
|
||||
}
|
||||
|
||||
private import Cached
|
||||
|
||||
/** Holds if `bb` is an entry basic block. */
|
||||
private predicate entryBB(BasicBlock bb) { bb.getFirstNode() instanceof EntryNode }
|
||||
|
||||
/**
|
||||
* An entry basic block, that is, a basic block whose first node is
|
||||
* an entry node.
|
||||
*/
|
||||
class EntryBasicBlock extends BasicBlock {
|
||||
EntryBasicBlock() { entryBB(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An annotated exit basic block, that is, a basic block whose last node is
|
||||
* an annotated exit node.
|
||||
*/
|
||||
class AnnotatedExitBasicBlock extends BasicBlock {
|
||||
private boolean normal;
|
||||
|
||||
AnnotatedExitBasicBlock() {
|
||||
exists(AnnotatedExitNode n |
|
||||
n = this.getANode() and
|
||||
if n.isNormal() then normal = true else normal = false
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this block represent a normal exit. */
|
||||
final predicate isNormal() { normal = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* An exit basic block, that is, a basic block whose last node is
|
||||
* an exit node.
|
||||
*/
|
||||
class ExitBasicBlock extends BasicBlock {
|
||||
ExitBasicBlock() { this.getLastNode() instanceof ExitNode }
|
||||
}
|
||||
|
||||
/*
|
||||
* private module JoinBlockPredecessors {
|
||||
* private predicate id(AstNode x, AstNode y) { x = y }
|
||||
*
|
||||
* private predicate idOf(AstNode x, int y) = equivalenceRelation(id/2)(x, y)
|
||||
*
|
||||
* int getId(JoinBlockPredecessor jbp) {
|
||||
* idOf(Ast::toTreeSitter(jbp.getFirstNode().(AstCfgNode).getAstNode()), result)
|
||||
* or
|
||||
* idOf(Ast::toTreeSitter(jbp.(EntryBasicBlock).getScope()), result)
|
||||
* }
|
||||
*
|
||||
* string getSplitString(JoinBlockPredecessor jbp) {
|
||||
* result = jbp.getFirstNode().(AstCfgNode).getSplitsString()
|
||||
* or
|
||||
* not exists(jbp.getFirstNode().(AstCfgNode).getSplitsString()) and
|
||||
* result = ""
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
/** A basic block with more than one predecessor. */
|
||||
class JoinBlock extends BasicBlock {
|
||||
JoinBlock() { this.getFirstNode().isJoin() }
|
||||
|
||||
/**
|
||||
* Gets the `i`th predecessor of this join block, with respect to some
|
||||
* arbitrary order.
|
||||
*/
|
||||
JoinBlockPredecessor getJoinBlockPredecessor(int i) { result = getJoinBlockPredecessor(this, i) }
|
||||
}
|
||||
|
||||
/** A basic block that is an immediate predecessor of a join block. */
|
||||
class JoinBlockPredecessor extends BasicBlock {
|
||||
JoinBlockPredecessor() { this.getASuccessor() instanceof JoinBlock }
|
||||
}
|
||||
|
||||
/** A basic block that terminates in a condition, splitting the subsequent control flow. */
|
||||
class ConditionBlock extends BasicBlock {
|
||||
ConditionBlock() { this.getLastNode().isCondition() }
|
||||
|
||||
/**
|
||||
* Holds if basic block `succ` is immediately controlled by this basic
|
||||
* block with conditional value `s`. That is, `succ` is an immediate
|
||||
* successor of this block, and `succ` can only be reached from
|
||||
* the callable entry point by going via the `s` edge out of this basic block.
|
||||
*/
|
||||
predicate immediatelyControls(BasicBlock succ, BooleanSuccessor s) {
|
||||
immediatelyControls(this, succ, s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if basic block `controlled` is controlled by this basic block with
|
||||
* conditional value `s`. That is, `controlled` can only be reached from
|
||||
* the callable entry point by going via the `s` edge out of this basic block.
|
||||
*/
|
||||
predicate controls(BasicBlock controlled, BooleanSuccessor s) { controls(this, controlled, s) }
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.controlflow.Cfg as CfgShared
|
||||
private import codeql.Locations
|
||||
|
||||
module Completion {
|
||||
private newtype TCompletion =
|
||||
TSimpleCompletion() or
|
||||
TBooleanCompletion(boolean b) { b in [false, true] } or
|
||||
TReturnCompletion()
|
||||
|
||||
abstract class Completion extends TCompletion {
|
||||
abstract string toString();
|
||||
|
||||
predicate isValidForSpecific(AstNode e) { none() }
|
||||
|
||||
predicate isValidFor(AstNode e) { this.isValidForSpecific(e) }
|
||||
|
||||
abstract SuccessorType getAMatchingSuccessorType();
|
||||
}
|
||||
|
||||
abstract class NormalCompletion extends Completion { }
|
||||
|
||||
class SimpleCompletion extends NormalCompletion, TSimpleCompletion {
|
||||
override string toString() { result = "SimpleCompletion" }
|
||||
|
||||
override predicate isValidFor(AstNode e) { not any(Completion c).isValidForSpecific(e) }
|
||||
|
||||
override NormalSuccessor getAMatchingSuccessorType() { any() }
|
||||
}
|
||||
|
||||
class BooleanCompletion extends NormalCompletion, TBooleanCompletion {
|
||||
boolean value;
|
||||
|
||||
BooleanCompletion() { this = TBooleanCompletion(value) }
|
||||
|
||||
override string toString() { result = "BooleanCompletion(" + value + ")" }
|
||||
|
||||
override predicate isValidForSpecific(AstNode e) { none() }
|
||||
|
||||
override BooleanSuccessor getAMatchingSuccessorType() { result.getValue() = value }
|
||||
|
||||
final boolean getValue() { result = value }
|
||||
}
|
||||
|
||||
class ReturnCompletion extends Completion, TReturnCompletion {
|
||||
override string toString() { result = "ReturnCompletion" }
|
||||
|
||||
override predicate isValidForSpecific(AstNode e) { none() }
|
||||
|
||||
override ReturnSuccessor getAMatchingSuccessorType() { any() }
|
||||
}
|
||||
|
||||
cached
|
||||
private newtype TSuccessorType =
|
||||
TNormalSuccessor() or
|
||||
TBooleanSuccessor(boolean b) { b in [false, true] } or
|
||||
TReturnSuccessor()
|
||||
|
||||
class SuccessorType extends TSuccessorType {
|
||||
string toString() { none() }
|
||||
}
|
||||
|
||||
class NormalSuccessor extends SuccessorType, TNormalSuccessor {
|
||||
override string toString() { result = "successor" }
|
||||
}
|
||||
|
||||
class BooleanSuccessor extends SuccessorType, TBooleanSuccessor {
|
||||
boolean value;
|
||||
|
||||
BooleanSuccessor() { this = TBooleanSuccessor(value) }
|
||||
|
||||
override string toString() { result = value.toString() }
|
||||
|
||||
boolean getValue() { result = value }
|
||||
}
|
||||
|
||||
class ReturnSuccessor extends SuccessorType, TReturnSuccessor {
|
||||
override string toString() { result = "return" }
|
||||
}
|
||||
}
|
||||
|
||||
module CfgScope {
|
||||
abstract class CfgScope extends AstNode { }
|
||||
|
||||
class WorkflowScope extends CfgScope instanceof Workflow { }
|
||||
|
||||
class CompositeActionScope extends CfgScope instanceof CompositeAction { }
|
||||
}
|
||||
|
||||
private module Implementation implements CfgShared::InputSig<Location> {
|
||||
import codeql.actions.Ast
|
||||
import Completion
|
||||
import CfgScope
|
||||
|
||||
predicate completionIsNormal(Completion c) { not c instanceof ReturnCompletion }
|
||||
|
||||
// Not using CFG splitting, so the following are just dummy types.
|
||||
private newtype TUnit = Unit()
|
||||
|
||||
additional class SplitKindBase = TUnit;
|
||||
|
||||
additional class Split extends TUnit {
|
||||
abstract string toString();
|
||||
}
|
||||
|
||||
predicate completionIsSimple(Completion c) { c instanceof SimpleCompletion }
|
||||
|
||||
predicate completionIsValidFor(Completion c, AstNode e) { c.isValidFor(e) }
|
||||
|
||||
CfgScope getCfgScope(AstNode e) {
|
||||
exists(AstNode p | p = e.getParentNode() |
|
||||
result = p
|
||||
or
|
||||
not p instanceof CfgScope and result = getCfgScope(p)
|
||||
)
|
||||
}
|
||||
|
||||
additional int maxSplits() { result = 0 }
|
||||
|
||||
predicate scopeFirst(CfgScope scope, AstNode e) {
|
||||
first(scope.(Workflow), e) or
|
||||
first(scope.(CompositeAction), e)
|
||||
}
|
||||
|
||||
predicate scopeLast(CfgScope scope, AstNode e, Completion c) {
|
||||
last(scope.(Workflow), e, c) or
|
||||
last(scope.(CompositeAction), e, c)
|
||||
}
|
||||
|
||||
predicate successorTypeIsSimple(SuccessorType t) { t instanceof NormalSuccessor }
|
||||
|
||||
predicate successorTypeIsCondition(SuccessorType t) { t instanceof BooleanSuccessor }
|
||||
|
||||
SuccessorType getAMatchingSuccessorType(Completion c) { result = c.getAMatchingSuccessorType() }
|
||||
|
||||
predicate isAbnormalExitType(SuccessorType t) { none() }
|
||||
|
||||
int idOfAstNode(AstNode node) { none() }
|
||||
|
||||
int idOfCfgScope(CfgScope scope) { none() }
|
||||
}
|
||||
|
||||
module CfgImpl = CfgShared::Make<Location, Implementation>;
|
||||
|
||||
private import CfgImpl
|
||||
private import Completion
|
||||
private import CfgScope
|
||||
|
||||
private class CompositeActionTree extends StandardPreOrderTree instanceof CompositeAction {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = this.(CompositeAction).getAnInput() or
|
||||
child = this.(CompositeAction).getOutputs() or
|
||||
child = this.(CompositeAction).getRuns()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RunsTree extends StandardPreOrderTree instanceof Runs {
|
||||
override ControlFlowTree getChildNode(int i) { result = super.getStep(i) }
|
||||
}
|
||||
|
||||
private class WorkflowTree extends StandardPreOrderTree instanceof Workflow {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
if this instanceof ReusableWorkflow
|
||||
then
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = this.(ReusableWorkflow).getAnInput() or
|
||||
child = this.(ReusableWorkflow).getOutputs() or
|
||||
child = this.(ReusableWorkflow).getStrategy() or
|
||||
child = this.(ReusableWorkflow).getAJob()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
else
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = super.getStrategy() or
|
||||
child = super.getAJob()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class OutputsTree extends StandardPreOrderTree instanceof Outputs {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
child = super.getAnOutputExpr() and l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class StrategyTree extends StandardPreOrderTree instanceof Strategy {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
child = super.getAMatrixVarExpr() and l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class JobTree extends StandardPreOrderTree instanceof LocalJob {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = super.getAStep() or
|
||||
child = super.getOutputs() or
|
||||
child = super.getStrategy()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExternalJobTree extends StandardPreOrderTree instanceof ExternalJob {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = super.getArgumentExpr(_) or
|
||||
child = super.getInScopeEnvVarExpr(_) or
|
||||
child = super.getOutputs() or
|
||||
child = super.getStrategy()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class UsesTree extends StandardPreOrderTree instanceof UsesStep {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(child = super.getArgumentExpr(_) or child = super.getInScopeEnvVarExpr(_)) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RunTree extends StandardPreOrderTree instanceof Run {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](AstNode child, Location l |
|
||||
(
|
||||
child = super.getInScopeEnvVarExpr(_) or
|
||||
child = super.getAnScriptExpr() or
|
||||
child = super.getScript()
|
||||
) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ScalarValueTree extends StandardPreOrderTree instanceof ScalarValue {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](Expression child, Location l |
|
||||
child = super.getAChildNode() and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class UsesLeaf extends LeafTree instanceof Uses { }
|
||||
|
||||
private class InputTree extends LeafTree instanceof Input { }
|
||||
|
||||
private class ScalarValueLeaf extends LeafTree instanceof ScalarValue { }
|
||||
|
||||
private class ExpressionLeaf extends LeafTree instanceof Expression { }
|
||||
@@ -1,131 +0,0 @@
|
||||
private import actions
|
||||
private import internal.ExternalFlowExtensions as Extensions
|
||||
private import codeql.actions.DataFlow
|
||||
private import codeql.actions.security.ArtifactPoisoningQuery
|
||||
|
||||
/**
|
||||
* MaD sources
|
||||
* Fields:
|
||||
* - action: Fully-qualified action name (NWO)
|
||||
* - version: Either '*' or a specific SHA/Tag
|
||||
* - output arg: To node (prefixed with either `env.` or `output.`)
|
||||
* - provenance: verification of the model
|
||||
*/
|
||||
predicate actionsSourceModel(
|
||||
string action, string version, string output, string kind, string provenance
|
||||
) {
|
||||
Extensions::actionsSourceModel(action, version, output, kind, provenance)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD summaries
|
||||
* Fields:
|
||||
* - action: Fully-qualified action name (NWO)
|
||||
* - version: Either '*' or a specific SHA/Tag
|
||||
* - input arg: From node (prefixed with either `env.` or `input.`)
|
||||
* - output arg: To node (prefixed with either `env.` or `output.`)
|
||||
* - kind: Either 'Taint' or 'Value'
|
||||
* - provenance: verification of the model
|
||||
*/
|
||||
predicate actionsSummaryModel(
|
||||
string action, string version, string input, string output, string kind, string provenance
|
||||
) {
|
||||
Extensions::actionsSummaryModel(action, version, input, output, kind, provenance)
|
||||
}
|
||||
|
||||
/**
|
||||
* MaD sinks
|
||||
* Fields:
|
||||
* - action: Fully-qualified action name (NWO)
|
||||
* - version: Either '*' or a specific SHA/Tag
|
||||
* - input: sink node (prefixed with either `env.` or `input.`)
|
||||
* - kind: sink kind
|
||||
* - provenance: verification of the model
|
||||
*/
|
||||
predicate actionsSinkModel(
|
||||
string action, string version, string input, string kind, string provenance
|
||||
) {
|
||||
Extensions::actionsSinkModel(action, version, input, kind, provenance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if source.fieldName is a MaD-defined source of a given taint kind.
|
||||
*/
|
||||
predicate madSource(DataFlow::Node source, string kind, string fieldName) {
|
||||
exists(Uses uses, string action, string version |
|
||||
actionsSourceModel(action, version, fieldName, kind, _) and
|
||||
uses.getCallee() = action.toLowerCase() and
|
||||
(
|
||||
if version.trim() = "*"
|
||||
then uses.getVersion() = any(string v)
|
||||
else uses.getVersion() = version.trim()
|
||||
) and
|
||||
(
|
||||
if fieldName.trim().matches("env.%")
|
||||
then source.asExpr() = uses.getInScopeEnvVarExpr(fieldName.trim().replaceAll("env.", ""))
|
||||
else
|
||||
if fieldName.trim().matches("output.%")
|
||||
then source.asExpr() = uses
|
||||
else none()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the data flow from `pred` to `succ` is a MaD store step.
|
||||
*/
|
||||
predicate madStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
|
||||
exists(Uses uses, string action, string version, string input, string output |
|
||||
actionsSummaryModel(action, version, input, output, "taint", _) and
|
||||
c = any(DataFlow::FieldContent ct | ct.getName() = output.replaceAll("output.", "")) and
|
||||
uses.getCallee() = action.toLowerCase() and
|
||||
// version check
|
||||
(
|
||||
if version.trim() = "*"
|
||||
then uses.getVersion() = any(string v)
|
||||
else uses.getVersion() = version.trim()
|
||||
) and
|
||||
// pred provenance
|
||||
(
|
||||
input.trim().matches("env.%") and
|
||||
pred.asExpr() = uses.getInScopeEnvVarExpr(input.trim().replaceAll("env.", ""))
|
||||
or
|
||||
input.trim().matches("input.%") and
|
||||
pred.asExpr() = uses.getArgumentExpr(input.trim().replaceAll("input.", ""))
|
||||
or
|
||||
input.trim() = "artifact" and
|
||||
exists(UntrustedArtifactDownloadStep download |
|
||||
pred.asExpr() = download and
|
||||
download.getAFollowingStep() = uses
|
||||
)
|
||||
) and
|
||||
succ.asExpr() = uses
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if sink is a MaD-defined sink for a given taint kind.
|
||||
*/
|
||||
predicate madSink(DataFlow::Node sink, string kind) {
|
||||
exists(Uses uses, string action, string version, string input |
|
||||
actionsSinkModel(action, version, input, kind, _) and
|
||||
uses.getCallee() = action.toLowerCase() and
|
||||
// version check
|
||||
(
|
||||
if version.trim() = "*"
|
||||
then uses.getVersion() = any(string v)
|
||||
else uses.getVersion() = version.trim()
|
||||
) and
|
||||
// pred provenance
|
||||
(
|
||||
input.trim().matches("env.%") and
|
||||
sink.asExpr() = uses.getInScopeEnvVarExpr(input.trim().replaceAll("env.", ""))
|
||||
or
|
||||
input.trim().matches("input.%") and
|
||||
sink.asExpr() = uses.getArgumentExpr(input.trim().replaceAll("input.", ""))
|
||||
or
|
||||
input.trim() = "artifact" and
|
||||
sink.asExpr() = uses
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
private import codeql.actions.security.ArtifactPoisoningQuery
|
||||
private import codeql.actions.security.UntrustedCheckoutQuery
|
||||
private import codeql.actions.config.Config
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
|
||||
/**
|
||||
* A data flow source.
|
||||
*/
|
||||
abstract class SourceNode extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a string that represents the source kind with respect to threat modeling.
|
||||
*/
|
||||
abstract string getThreatModel();
|
||||
}
|
||||
|
||||
/** A data flow source of remote user input. */
|
||||
abstract class RemoteFlowSource extends SourceNode {
|
||||
/** Gets a string that describes the type of this remote flow source. */
|
||||
abstract string getSourceType();
|
||||
|
||||
/** Gets the event that triggered the source. */
|
||||
abstract string getEventName();
|
||||
|
||||
override string getThreatModel() { result = "remote" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow source of user input from github context.
|
||||
* eg: github.head_ref
|
||||
*/
|
||||
class GitHubCtxSource extends RemoteFlowSource {
|
||||
string flag;
|
||||
string event;
|
||||
GitHubExpression e;
|
||||
|
||||
GitHubCtxSource() {
|
||||
this.asExpr() = e and
|
||||
// github.head_ref
|
||||
e.getFieldName() = "head_ref" and
|
||||
flag = "branch" and
|
||||
(
|
||||
event = e.getATriggerEvent().getName() and
|
||||
event = "pull_request_target"
|
||||
or
|
||||
not exists(e.getATriggerEvent()) and
|
||||
event = "unknown"
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
override string getEventName() { result = event }
|
||||
}
|
||||
|
||||
class GitHubEventCtxSource extends RemoteFlowSource {
|
||||
string flag;
|
||||
string context;
|
||||
string event;
|
||||
|
||||
GitHubEventCtxSource() {
|
||||
exists(Expression e, string regexp |
|
||||
this.asExpr() = e and
|
||||
context = e.getExpression() and
|
||||
(
|
||||
// the context is available for the job trigger events
|
||||
event = e.getATriggerEvent().getName() and
|
||||
exists(string context_prefix |
|
||||
contextTriggerDataModel(event, context_prefix) and
|
||||
normalizeExpr(context).matches("%" + context_prefix + "%")
|
||||
)
|
||||
or
|
||||
not exists(e.getATriggerEvent()) and
|
||||
event = "unknown"
|
||||
) and
|
||||
untrustedEventPropertiesDataModel(regexp, flag) and
|
||||
not flag = "json" and
|
||||
normalizeExpr(context).regexpMatch("(?i)\\s*" + wrapRegexp(regexp) + ".*")
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
string getContext() { result = context }
|
||||
|
||||
override string getEventName() { result = event }
|
||||
}
|
||||
|
||||
abstract class CommandSource extends RemoteFlowSource {
|
||||
abstract string getCommand();
|
||||
|
||||
abstract Run getEnclosingRun();
|
||||
|
||||
override string getEventName() { result = this.getEnclosingRun().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
class GitCommandSource extends RemoteFlowSource, CommandSource {
|
||||
Run run;
|
||||
string cmd;
|
||||
string flag;
|
||||
|
||||
GitCommandSource() {
|
||||
exists(Step checkout, string cmd_regex |
|
||||
checkout instanceof SimplePRHeadCheckoutStep and
|
||||
this.asExpr() = run.getScript() and
|
||||
checkout.getAFollowingStep() = run and
|
||||
run.getScript().getAStmt() = cmd and
|
||||
cmd.indexOf("git") = 0 and
|
||||
untrustedGitCommandDataModel(cmd_regex, flag) and
|
||||
cmd.regexpMatch(cmd_regex + ".*")
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
override string getCommand() { result = cmd }
|
||||
|
||||
override Run getEnclosingRun() { result = run }
|
||||
}
|
||||
|
||||
class GhCLICommandSource extends RemoteFlowSource, CommandSource {
|
||||
Run run;
|
||||
string cmd;
|
||||
string flag;
|
||||
|
||||
GhCLICommandSource() {
|
||||
exists(string cmd_regex |
|
||||
this.asExpr() = run.getScript() and
|
||||
run.getScript().getAStmt() = cmd and
|
||||
cmd.indexOf("gh ") = 0 and
|
||||
untrustedGhCommandDataModel(cmd_regex, flag) and
|
||||
cmd.regexpMatch(cmd_regex + ".*") and
|
||||
(
|
||||
cmd.regexpMatch(".*\\b(pr|pulls)\\b.*") and
|
||||
run.getATriggerEvent().getName() = checkoutTriggers()
|
||||
or
|
||||
not cmd.regexpMatch(".*\\b(pr|pulls)\\b.*")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
override Run getEnclosingRun() { result = run }
|
||||
|
||||
override string getCommand() { result = cmd }
|
||||
}
|
||||
|
||||
class GitHubEventPathSource extends RemoteFlowSource, CommandSource {
|
||||
string cmd;
|
||||
string flag;
|
||||
string access_path;
|
||||
Run run;
|
||||
|
||||
// Examples
|
||||
// COMMENT_AUTHOR=$(jq -r .comment.user.login "$GITHUB_EVENT_PATH")
|
||||
// CURRENT_COMMENT=$(jq -r .comment.body "$GITHUB_EVENT_PATH")
|
||||
// PR_HEAD=$(jq --raw-output .pull_request.head.ref ${GITHUB_EVENT_PATH})
|
||||
// PR_NUMBER=$(jq --raw-output .pull_request.number ${GITHUB_EVENT_PATH})
|
||||
// PR_TITLE=$(jq --raw-output .pull_request.title ${GITHUB_EVENT_PATH})
|
||||
// BODY=$(jq -r '.issue.body' "$GITHUB_EVENT_PATH" | sed -n '3p')
|
||||
GitHubEventPathSource() {
|
||||
this.asExpr() = run.getScript() and
|
||||
run.getScript().getACommand() = cmd and
|
||||
cmd.matches("jq%") and
|
||||
cmd.matches("%GITHUB_EVENT_PATH%") and
|
||||
exists(string regexp |
|
||||
untrustedEventPropertiesDataModel(regexp, flag) and
|
||||
not flag = "json" and
|
||||
access_path = "github.event" + cmd.regexpCapture(".*\\s+([^\\s]+)\\s+.*", 1) and
|
||||
normalizeExpr(access_path).regexpMatch("(?i)\\s*" + wrapRegexp(regexp) + ".*")
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
override string getCommand() { result = cmd }
|
||||
|
||||
override Run getEnclosingRun() { result = run }
|
||||
}
|
||||
|
||||
class GitHubEventJsonSource extends RemoteFlowSource {
|
||||
string flag;
|
||||
string event;
|
||||
|
||||
GitHubEventJsonSource() {
|
||||
exists(Expression e, string context, string regexp |
|
||||
this.asExpr() = e and
|
||||
context = e.getExpression() and
|
||||
untrustedEventPropertiesDataModel(regexp, _) and
|
||||
(
|
||||
// only contexts for the triggering events are considered tainted.
|
||||
// eg: for `pull_request`, we only consider `github.event.pull_request`
|
||||
event = e.getEnclosingWorkflow().getATriggerEvent().getName() and
|
||||
exists(string context_prefix |
|
||||
contextTriggerDataModel(event, context_prefix) and
|
||||
normalizeExpr(context).matches("%" + context_prefix + "%")
|
||||
) and
|
||||
normalizeExpr(context).regexpMatch("(?i).*" + wrapJsonRegexp(regexp) + ".*")
|
||||
or
|
||||
// github.event is tainted for all triggers
|
||||
event = e.getEnclosingWorkflow().getATriggerEvent().getName() and
|
||||
contextTriggerDataModel(e.getEnclosingWorkflow().getATriggerEvent().getName(), _) and
|
||||
normalizeExpr(context).regexpMatch("(?i).*" + wrapJsonRegexp("\\bgithub.event\\b") + ".*")
|
||||
or
|
||||
not exists(e.getATriggerEvent()) and
|
||||
event = "unknown"
|
||||
) and
|
||||
flag = "json"
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = flag }
|
||||
|
||||
override string getEventName() { result = event }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Source of untrusted data defined in a MaD specification
|
||||
*/
|
||||
class MaDSource extends RemoteFlowSource {
|
||||
string sourceType;
|
||||
|
||||
MaDSource() { madSource(this, sourceType, _) }
|
||||
|
||||
override string getSourceType() { result = sourceType }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
abstract class FileSource extends RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* A downloaded artifact.
|
||||
*/
|
||||
class ArtifactSource extends RemoteFlowSource, FileSource {
|
||||
ArtifactSource() { this.asExpr() instanceof UntrustedArtifactDownloadStep }
|
||||
|
||||
override string getSourceType() { result = "artifact" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A file from an untrusted checkout.
|
||||
*/
|
||||
private class CheckoutSource extends RemoteFlowSource, FileSource {
|
||||
CheckoutSource() { this.asExpr() instanceof SimplePRHeadCheckoutStep }
|
||||
|
||||
override string getSourceType() { result = "artifact" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of file names returned by dorny/paths-filter.
|
||||
*/
|
||||
class DornyPathsFilterSource extends RemoteFlowSource {
|
||||
DornyPathsFilterSource() {
|
||||
exists(UsesStep u |
|
||||
u.getCallee() = "dorny/paths-filter" and
|
||||
u.getArgument("list-files") = ["csv", "json"] and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "filename" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of file names returned by tj-actions/changed-files.
|
||||
*/
|
||||
class TJActionsChangedFilesSource extends RemoteFlowSource {
|
||||
TJActionsChangedFilesSource() {
|
||||
exists(UsesStep u, string vulnerable_action, string vulnerable_version, string vulnerable_sha |
|
||||
vulnerableActionsDataModel(vulnerable_action, vulnerable_version, vulnerable_sha, _) and
|
||||
u.getCallee() = "tj-actions/changed-files" and
|
||||
u.getCallee() = vulnerable_action and
|
||||
(
|
||||
u.getArgument("safe_output") = "false"
|
||||
or
|
||||
(u.getVersion() = vulnerable_version or u.getVersion() = vulnerable_sha)
|
||||
) and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "filename" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of file names returned by tj-actions/verify-changed-files.
|
||||
*/
|
||||
class TJActionsVerifyChangedFilesSource extends RemoteFlowSource {
|
||||
TJActionsVerifyChangedFilesSource() {
|
||||
exists(UsesStep u, string vulnerable_action, string vulnerable_version, string vulnerable_sha |
|
||||
vulnerableActionsDataModel(vulnerable_action, vulnerable_version, vulnerable_sha, _) and
|
||||
u.getCallee() = "tj-actions/verify-changed-files" and
|
||||
u.getCallee() = vulnerable_action and
|
||||
(
|
||||
u.getArgument("safe_output") = "false"
|
||||
or
|
||||
(u.getVersion() = vulnerable_version or u.getVersion() = vulnerable_sha)
|
||||
) and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "filename" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
class Xt0rtedSlashCommandSource extends RemoteFlowSource {
|
||||
Xt0rtedSlashCommandSource() {
|
||||
exists(UsesStep u |
|
||||
u.getCallee() = "xt0rted/slash-command-action" and
|
||||
u.getArgument("permission-level").toLowerCase() = ["read", "none"] and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "text" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
class ZenteredIssueFormBodyParserSource extends RemoteFlowSource {
|
||||
ZenteredIssueFormBodyParserSource() {
|
||||
exists(UsesStep u |
|
||||
u.getCallee() = "zentered/issue-forms-body-parser" and
|
||||
not exists(u.getArgument("body")) and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "text" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
|
||||
class OctokitRequestActionSource extends RemoteFlowSource {
|
||||
OctokitRequestActionSource() {
|
||||
exists(UsesStep u, string route |
|
||||
u.getCallee() = "octokit/request-action" and
|
||||
route = u.getArgument("route").trim() and
|
||||
route.indexOf("GET") = 0 and
|
||||
(
|
||||
route.matches("%/commits%") or
|
||||
route.matches("%/comments%") or
|
||||
route.matches("%/pulls%") or
|
||||
route.matches("%/issues%") or
|
||||
route.matches("%/users%") or
|
||||
route.matches("%github.event.issue.pull_request.url%")
|
||||
) and
|
||||
this.asExpr() = u
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "text" }
|
||||
|
||||
override string getEventName() { result = this.asExpr().getATriggerEvent().getName() }
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Provides classes representing various flow steps for taint tracking.
|
||||
*/
|
||||
|
||||
private import actions
|
||||
private import codeql.actions.DataFlow
|
||||
private import codeql.actions.dataflow.FlowSources
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it in its script and sets an output in its script.
|
||||
* e.g.
|
||||
* - name: Extract and Clean Initial URL
|
||||
* id: extract-url
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* echo "::set-output name=foo::$BODY"
|
||||
* echo "foo=$(echo $BODY)" >> $GITHUB_OUTPUT
|
||||
* echo "foo=$(echo $BODY)" >> "$GITHUB_OUTPUT"
|
||||
* echo "::set-output name=step-output::$BODY"
|
||||
*/
|
||||
predicate envToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
|
||||
exists(Run run, string var, string field |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run and
|
||||
run.getScript().getAnEnvReachingGitHubOutputWrite(var, field) and
|
||||
c = any(DataFlow::FieldContent ct | ct.getName() = field)
|
||||
)
|
||||
}
|
||||
|
||||
predicate envToEnvStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
|
||||
exists(
|
||||
Run run, string var, string field //string key, string value |
|
||||
|
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
// we store the taint on the enclosing job since the may not exist an implicit env attribute
|
||||
succ.asExpr() = run.getEnclosingJob() and
|
||||
run.getScript().getAnEnvReachingGitHubEnvWrite(var, field) and
|
||||
c = any(DataFlow::FieldContent ct | ct.getName() = field)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A command whose output gets assigned to an environment variable or step output.
|
||||
* - run: |
|
||||
* echo "foo=$(cmd)" >> "$GITHUB_OUTPUT"
|
||||
* - run: |
|
||||
* foo=$(<cmd)"
|
||||
* echo "bar=${foo}" >> "$GITHUB_OUTPUT"
|
||||
*/
|
||||
predicate commandToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
|
||||
exists(Run run, string key, string cmd |
|
||||
(
|
||||
exists(CommandSource source | source.getCommand() = cmd)
|
||||
or
|
||||
exists(FileSource source |
|
||||
source.asExpr().(Step).getAFollowingStep() = run and
|
||||
run.getScript().getAFileReadCommand() = cmd
|
||||
)
|
||||
) and
|
||||
run.getScript().getACmdReachingGitHubOutputWrite(cmd, key) and
|
||||
c = any(DataFlow::FieldContent ct | ct.getName() = key) and
|
||||
pred.asExpr() = run.getScript() and
|
||||
succ.asExpr() = run
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A command whose output gets assigned to an environment variable or step output.
|
||||
* - run: |
|
||||
* echo "foo=$(cmd)" >> "$GITHUB_ENV"
|
||||
* - run: |
|
||||
* foo=$(<cmd)"
|
||||
* echo "bar=${foo}" >> "$GITHUB_ENV"
|
||||
*/
|
||||
predicate commandToEnvStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
|
||||
exists(Run run, string key, string cmd |
|
||||
(
|
||||
exists(CommandSource source | source.getCommand() = cmd)
|
||||
or
|
||||
exists(FileSource source |
|
||||
source.asExpr().(Step).getAFollowingStep() = run and
|
||||
run.getScript().getAFileReadCommand() = cmd
|
||||
)
|
||||
) and
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(cmd, key) and
|
||||
c = any(DataFlow::FieldContent ct | ct.getName() = key) and
|
||||
pred.asExpr() = run.getScript() and
|
||||
// we store the taint on the enclosing job since there may not be an implicit env attribute
|
||||
succ.asExpr() = run.getEnclosingJob()
|
||||
)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* Provides classes representing various flow steps for taint tracking.
|
||||
*/
|
||||
|
||||
private import actions
|
||||
private import codeql.util.Unit
|
||||
private import codeql.actions.DataFlow
|
||||
private import codeql.actions.dataflow.FlowSources
|
||||
|
||||
/**
|
||||
* A unit class for adding additional taint steps.
|
||||
*
|
||||
* Extend this class to add additional taint steps that should apply to all
|
||||
* taint configurations.
|
||||
*/
|
||||
class AdditionalTaintStep extends Unit {
|
||||
/**
|
||||
* Holds if the step from `node1` to `node2` should be considered a taint
|
||||
* step for all configurations.
|
||||
*/
|
||||
abstract predicate step(DataFlow::Node node1, DataFlow::Node node2);
|
||||
}
|
||||
|
||||
/**
|
||||
* A file source step followed by a Run step may read the file.
|
||||
*/
|
||||
predicate fileDownloadToRunStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(FileSource source, Run run |
|
||||
pred = source and
|
||||
source.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of the _files field of the dorny/paths-filter action.
|
||||
*/
|
||||
predicate dornyPathsFilterTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof DornyPathsFilterSource and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
o.getFieldName().matches("%_files") and
|
||||
succ.asExpr() = o
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of user-controlled field of the tj-actions/changed-files action.
|
||||
*/
|
||||
predicate tjActionsChangedFilesTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof TJActionsChangedFilesSource and
|
||||
o.getTarget() = pred.asExpr() and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
o.getFieldName() =
|
||||
[
|
||||
"added_files", "copied_files", "deleted_files", "modified_files", "renamed_files",
|
||||
"all_old_new_renamed_files", "type_changed_files", "unmerged_files", "unknown_files",
|
||||
"all_changed_and_modified_files", "all_changed_files", "other_changed_files",
|
||||
"all_modified_files", "other_modified_files", "other_deleted_files", "modified_keys",
|
||||
"changed_keys"
|
||||
] and
|
||||
succ.asExpr() = o
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of user-controlled field of the tj-actions/verify-changed-files action.
|
||||
*/
|
||||
predicate tjActionsVerifyChangedFilesTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof TJActionsVerifyChangedFilesSource and
|
||||
o.getTarget() = pred.asExpr() and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
o.getFieldName() = "changed_files" and
|
||||
succ.asExpr() = o
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of user-controlled field of the xt0rted/slash-command-action action.
|
||||
*/
|
||||
predicate xt0rtedSlashCommandActionTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof Xt0rtedSlashCommandSource and
|
||||
o.getTarget() = pred.asExpr() and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
o.getFieldName() = "command-arguments" and
|
||||
succ.asExpr() = o
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of user-controlled field of the zentered/issue-forms-body-parser action.
|
||||
*/
|
||||
predicate zenteredIssueFormBodyParserSource(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof ZenteredIssueFormBodyParserSource and
|
||||
o.getTarget() = pred.asExpr() and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
(
|
||||
not o instanceof JsonReferenceExpression and
|
||||
o.getFieldName() = "data"
|
||||
or
|
||||
o instanceof JsonReferenceExpression and
|
||||
o.(JsonReferenceExpression).getInnerExpression().matches("%.data")
|
||||
) and
|
||||
succ.asExpr() = o
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read of user-controlled field of the octokit/request-action action.
|
||||
*/
|
||||
predicate octokitRequestActionTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(StepsExpression o |
|
||||
pred instanceof OctokitRequestActionSource and
|
||||
o.getTarget() = pred.asExpr() and
|
||||
o.getStepId() = pred.asExpr().(UsesStep).getId() and
|
||||
succ.asExpr() = o and
|
||||
(
|
||||
not o instanceof JsonReferenceExpression and
|
||||
o.getFieldName() = "data"
|
||||
or
|
||||
o instanceof JsonReferenceExpression and
|
||||
o.(JsonReferenceExpression).getInnerExpression().matches("%.data") and
|
||||
o.(JsonReferenceExpression)
|
||||
.getAccessPath()
|
||||
.matches([
|
||||
"%.title",
|
||||
"%.user.login",
|
||||
"%.body",
|
||||
"%.head.ref",
|
||||
"%.head.repo.full_name",
|
||||
"%.commit.author.email",
|
||||
"%.commit.commiter.email",
|
||||
"%.commit.message",
|
||||
"%.email",
|
||||
"%.name",
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class TaintSteps extends AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
dornyPathsFilterTaintStep(node1, node2) or
|
||||
tjActionsChangedFilesTaintStep(node1, node2) or
|
||||
tjActionsVerifyChangedFilesTaintStep(node1, node2) or
|
||||
xt0rtedSlashCommandActionTaintStep(node1, node2) or
|
||||
xt0rtedSlashCommandActionTaintStep(node1, node2) or
|
||||
zenteredIssueFormBodyParserSource(node1, node2) or
|
||||
octokitRequestActionTaintStep(node1, node2)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Provides Actions-specific definitions for use in the data flow library.
|
||||
* Implementation of https://github.com/github/codeql/blob/main/shared/dataflow/codeql/dataflow/DataFlow.qll
|
||||
*/
|
||||
|
||||
private import codeql.dataflow.DataFlow
|
||||
private import codeql.Locations
|
||||
|
||||
module ActionsDataFlow implements InputSig<Location> {
|
||||
import DataFlowPrivate as Private
|
||||
import DataFlowPublic
|
||||
import Private
|
||||
|
||||
predicate neverSkipInPathGraph = Private::neverSkipInPathGraph/1;
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
private import codeql.util.Unit
|
||||
private import codeql.dataflow.DataFlow
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.actions.Cfg as Cfg
|
||||
private import codeql.Locations
|
||||
private import codeql.actions.controlflow.BasicBlocks
|
||||
private import DataFlowPublic
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
private import codeql.actions.dataflow.FlowSteps
|
||||
private import codeql.actions.dataflow.FlowSources
|
||||
|
||||
class DataFlowSecondLevelScope = Unit;
|
||||
|
||||
cached
|
||||
newtype TNode = TExprNode(DataFlowExpr e)
|
||||
|
||||
class OutNode extends ExprNode {
|
||||
private DataFlowCall call;
|
||||
|
||||
OutNode() { call = this.getCfgNode() }
|
||||
|
||||
DataFlowCall getCall(ReturnKind kind) {
|
||||
result = call and
|
||||
kind instanceof NormalReturn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
class CastNode extends Node {
|
||||
CastNode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
class PostUpdateNode extends Node {
|
||||
PostUpdateNode() { none() }
|
||||
|
||||
Node getPreUpdateNode() { none() }
|
||||
}
|
||||
|
||||
predicate isParameterNode(ParameterNode p, DataFlowCallable c, ParameterPosition pos) {
|
||||
p.isParameterOf(c, pos)
|
||||
}
|
||||
|
||||
predicate isArgumentNode(ArgumentNode arg, DataFlowCall call, ArgumentPosition pos) {
|
||||
arg.argumentOf(call, pos)
|
||||
}
|
||||
|
||||
DataFlowCallable nodeGetEnclosingCallable(Node node) {
|
||||
node = TExprNode(any(DataFlowExpr e | result = e.getScope()))
|
||||
}
|
||||
|
||||
DataFlowType getNodeType(Node node) { any() }
|
||||
|
||||
predicate nodeIsHidden(Node node) { none() }
|
||||
|
||||
class DataFlowExpr extends Cfg::Node {
|
||||
DataFlowExpr() {
|
||||
this.getAstNode() instanceof Job or
|
||||
this.getAstNode() instanceof Expression or
|
||||
this.getAstNode() instanceof Uses or
|
||||
this.getAstNode() instanceof Run or
|
||||
this.getAstNode() instanceof Outputs or
|
||||
this.getAstNode() instanceof Input or
|
||||
this.getAstNode() instanceof ScalarValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call corresponds to a Uses steps where a composite action or a reusable workflow get called
|
||||
*/
|
||||
class DataFlowCall instanceof Cfg::Node {
|
||||
DataFlowCall() { super.getAstNode() instanceof Uses }
|
||||
|
||||
/** Gets a textual representation of this element. */
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
string getName() { result = super.getAstNode().(Uses).getCallee() }
|
||||
|
||||
DataFlowCallable getEnclosingCallable() { result = super.getScope() }
|
||||
|
||||
/** Gets a best-effort total ordering. */
|
||||
int totalorder() { none() }
|
||||
|
||||
/** Gets the location of this call. */
|
||||
Location getLocation() { result = this.(Cfg::Node).getLocation() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Cfg scope that can be called
|
||||
*/
|
||||
class DataFlowCallable instanceof Cfg::CfgScope {
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
string getName() {
|
||||
result = this.(ReusableWorkflowImpl).getResolvedPath() or
|
||||
result = this.(CompositeActionImpl).getResolvedPath()
|
||||
}
|
||||
|
||||
/** Gets a best-effort total ordering. */
|
||||
int totalorder() { none() }
|
||||
|
||||
/** Gets the location of this callable. */
|
||||
Location getLocation() { result = this.(Cfg::CfgScope).getLocation() }
|
||||
}
|
||||
|
||||
newtype TReturnKind = TNormalReturn()
|
||||
|
||||
abstract class ReturnKind extends TReturnKind {
|
||||
/** Gets a textual representation of this element. */
|
||||
abstract string toString();
|
||||
}
|
||||
|
||||
class NormalReturn extends ReturnKind, TNormalReturn {
|
||||
override string toString() { result = "return" }
|
||||
}
|
||||
|
||||
/** Gets a viable implementation of the target of the given `Call`. */
|
||||
DataFlowCallable viableCallable(DataFlowCall c) { c.getName() = result.getName() }
|
||||
|
||||
/**
|
||||
* Gets a node that can read the value returned from `call` with return kind
|
||||
* `kind`.
|
||||
*/
|
||||
OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) { call = result.getCall(kind) }
|
||||
|
||||
private newtype TDataFlowType = TUnknownDataFlowType()
|
||||
|
||||
/**
|
||||
* A type for a data flow node.
|
||||
*
|
||||
* This may or may not coincide with any type system existing for the source
|
||||
* language, but should minimally include unique types for individual closure
|
||||
* expressions (typically lambdas).
|
||||
*/
|
||||
class DataFlowType extends TDataFlowType {
|
||||
string toString() { result = "" }
|
||||
}
|
||||
|
||||
string ppReprType(DataFlowType t) { none() }
|
||||
|
||||
predicate compatibleTypes(DataFlowType t1, DataFlowType t2) { any() }
|
||||
|
||||
predicate typeStrongerThan(DataFlowType t1, DataFlowType t2) { none() }
|
||||
|
||||
newtype TContent =
|
||||
TFieldContent(string name) {
|
||||
// We only use field flow for env, steps and jobs outputs
|
||||
// not for accessing other context fields such as matrix or inputs
|
||||
name = any(StepsExpression a).getFieldName() or
|
||||
name = any(NeedsExpression a).getFieldName() or
|
||||
name = any(JobsExpression a).getFieldName() or
|
||||
name = any(EnvExpression a).getFieldName()
|
||||
}
|
||||
|
||||
predicate forceHighPrecision(Content c) { c instanceof FieldContent }
|
||||
|
||||
class NodeRegion instanceof Unit {
|
||||
string toString() { result = "NodeRegion" }
|
||||
|
||||
predicate contains(Node n) { none() }
|
||||
|
||||
int totalOrder() { result = 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the nodes in `nr` are unreachable when the call context is `call`.
|
||||
*/
|
||||
predicate isUnreachableInCall(NodeRegion nr, DataFlowCall call) { none() }
|
||||
|
||||
class ContentApprox = ContentSet;
|
||||
|
||||
ContentApprox getContentApprox(Content c) { result = c }
|
||||
|
||||
/**
|
||||
* Made a string to match the ArgumentPosition type.
|
||||
*/
|
||||
class ParameterPosition extends string {
|
||||
ParameterPosition() {
|
||||
exists(any(ReusableWorkflow w).getInput(this)) or
|
||||
exists(any(CompositeAction a).getInput(this))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Made a string to match `With:` keys in the AST
|
||||
*/
|
||||
class ArgumentPosition extends string {
|
||||
ArgumentPosition() { exists(any(Uses e).getArgumentExpr(this)) }
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step between a ${{ steps.xxx.outputs.yyy }} expression accesing a step output field
|
||||
* and the step output itself. But only for those cases where the step output is defined externally in a MaD Source
|
||||
* specification. The reason for this is that we don't currently have a way to specify that a source starts with a
|
||||
* non-empty access path so we cannot write a Source that stores the taint in a Content, we can only do that for steps
|
||||
* (storeStep). The easiest thing is to add this local flow step that simulates a read step from the source node for a specific
|
||||
* field name.
|
||||
*/
|
||||
predicate stepsCtxLocalStep(Node nodeFrom, Node nodeTo) {
|
||||
exists(Uses astFrom, StepsExpression astTo |
|
||||
madSource(nodeFrom, _, "output." + ["*", astTo.getFieldName()]) and
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
astTo.getTarget() = astFrom
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step between a ${{ needs.xxx.outputs.yyy }} expression accesing a job output field
|
||||
* and the step output itself. But only for those cases where the job (needs) output is defined externally in a MaD Source
|
||||
* specification. The reason for this is that we don't currently have a way to specify that a source starts with a
|
||||
* non-empty access path so we cannot write a Source that stores the taint in a Content, we can only do that for steps
|
||||
* (storeStep). The easiest thing is to add this local flow step that simulates a read step from the source node for a specific
|
||||
* field name.
|
||||
*/
|
||||
predicate needsCtxLocalStep(Node nodeFrom, Node nodeTo) {
|
||||
exists(Uses astFrom, NeedsExpression astTo |
|
||||
madSource(nodeFrom, _, "output." + astTo.getFieldName()) and
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
astTo.getTarget() = astFrom
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step between a ${{}} expression accesing an input variable and the input itself
|
||||
* e.g. ${{ inputs.foo }}
|
||||
*/
|
||||
predicate inputsCtxLocalStep(Node nodeFrom, Node nodeTo) {
|
||||
exists(AstNode astFrom, InputsExpression astTo |
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
astTo.getTarget() = astFrom
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step between a ${{}} expression accesing a matrix variable and the matrix itself
|
||||
* e.g. ${{ matrix.foo }}
|
||||
*/
|
||||
predicate matrixCtxLocalStep(Node nodeFrom, Node nodeTo) {
|
||||
exists(AstNode astFrom, MatrixExpression astTo |
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
astTo.getTarget() = astFrom
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step between a ${{}} expression accesing an env var and the var definition itself
|
||||
* e.g. ${{ env.foo }}
|
||||
*/
|
||||
predicate envCtxLocalStep(Node nodeFrom, Node nodeTo) {
|
||||
exists(AstNode astFrom, EnvExpression astTo |
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
(
|
||||
madSource(nodeFrom, _, "env." + astTo.getFieldName())
|
||||
or
|
||||
astTo.getTarget() = astFrom
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step from `nodeFrom` to `nodeTo`.
|
||||
* For Actions, we dont need SSA nodes since it should be already in SSA form
|
||||
* Local flow steps are always between two nodes in the same Cfg scope.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
predicate localFlowStep(Node nodeFrom, Node nodeTo) {
|
||||
stepsCtxLocalStep(nodeFrom, nodeTo) or
|
||||
needsCtxLocalStep(nodeFrom, nodeTo) or
|
||||
inputsCtxLocalStep(nodeFrom, nodeTo) or
|
||||
matrixCtxLocalStep(nodeFrom, nodeTo) or
|
||||
envCtxLocalStep(nodeFrom, nodeTo)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the local flow predicate that is used as a building block in global
|
||||
* data flow.
|
||||
*/
|
||||
cached
|
||||
predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo, string model) {
|
||||
localFlowStep(nodeFrom, nodeTo) and model = ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `node1` to `node2` through a non-local step
|
||||
* that does not follow a call edge. For example, a step through a global
|
||||
* variable.
|
||||
* We throw away the call context and let us jump to any location
|
||||
* AKA teleport steps
|
||||
* local steps are preferible since they are more predictable and easier to control
|
||||
*/
|
||||
predicate jumpStep(Node nodeFrom, Node nodeTo) { none() }
|
||||
|
||||
/**
|
||||
* Holds if a Expression reads a field from a job (needs/jobs), step (steps) output via a read of `c` (fieldname)
|
||||
*/
|
||||
predicate ctxFieldReadStep(Node node1, Node node2, ContentSet c) {
|
||||
exists(SimpleReferenceExpression access |
|
||||
(
|
||||
access instanceof NeedsExpression or
|
||||
access instanceof StepsExpression or
|
||||
access instanceof JobsExpression or
|
||||
access instanceof EnvExpression
|
||||
) and
|
||||
c = any(FieldContent ct | ct.getName() = access.getFieldName()) and
|
||||
node1.asExpr() = access.getTarget() and
|
||||
node2.asExpr() = access
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `node1` to `node2` via a read of `c`. Thus,
|
||||
* `node1` references an object with a content `c.getAReadContent()` whose
|
||||
* value ends up in `node2`.
|
||||
* Store steps without corresponding reads are pruned aggressively very early, since they can never contribute to a complete path.
|
||||
*/
|
||||
predicate readStep(Node node1, ContentSet c, Node node2) { ctxFieldReadStep(node1, node2, c) }
|
||||
|
||||
/**
|
||||
* Stores an output expression (node1) into its OutputsStm node (node2)
|
||||
* using the output variable name as the access path
|
||||
*/
|
||||
predicate fieldStoreStep(Node node1, Node node2, ContentSet c) {
|
||||
exists(Outputs out, string fieldName |
|
||||
node1.asExpr() = out.getOutputExpr(fieldName) and
|
||||
node2.asExpr() = out and
|
||||
c = any(FieldContent ct | ct.getName() = fieldName)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `node1` to `node2` via a store into `c`. Thus,
|
||||
* `node2` references an object with a content `c.getAStoreContent()` that
|
||||
* contains the value of `node1`.
|
||||
* Store steps without corresponding reads are pruned aggressively very early, since they can never contribute to a complete path.
|
||||
*/
|
||||
predicate storeStep(Node node1, ContentSet c, Node node2) {
|
||||
fieldStoreStep(node1, node2, c) or
|
||||
madStoreStep(node1, node2, c) or
|
||||
envToOutputStoreStep(node1, node2, c) or
|
||||
envToEnvStoreStep(node1, node2, c) or
|
||||
commandToOutputStoreStep(node1, node2, c) or
|
||||
commandToEnvStoreStep(node1, node2, c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if values stored inside content `c` are cleared at node `n`. For example,
|
||||
* any value stored inside `f` is cleared at the pre-update node associated with `x`
|
||||
* in `x.f = newValue`.
|
||||
*/
|
||||
predicate clearsContent(Node n, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Holds if the value that is being tracked is expected to be stored inside content `c`
|
||||
* at node `n`.
|
||||
*/
|
||||
predicate expectsContent(Node n, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Holds if flow is allowed to pass from parameter `p` and back to itself as a
|
||||
* side-effect, resulting in a summary from `p` to itself.
|
||||
*
|
||||
* One example would be to allow flow like `p.foo = p.bar;`, which is disallowed
|
||||
* by default as a heuristic.
|
||||
*/
|
||||
predicate allowParameterReturnInSelf(ParameterNode p) { none() }
|
||||
|
||||
predicate localMustFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFrom, nodeTo) }
|
||||
|
||||
private newtype TLambdaCallKind = TNone()
|
||||
|
||||
class LambdaCallKind = TLambdaCallKind;
|
||||
|
||||
/** Holds if `creation` is an expression that creates a lambda of kind `kind` for `c`. */
|
||||
predicate lambdaCreation(Node creation, LambdaCallKind kind, DataFlowCallable c) { none() }
|
||||
|
||||
/** Holds if `call` is a lambda call of kind `kind` where `receiver` is the lambda expression. */
|
||||
predicate lambdaCall(DataFlowCall call, LambdaCallKind kind, Node receiver) { none() }
|
||||
|
||||
/** Extra data-flow steps needed for lambda flow analysis. */
|
||||
predicate additionalLambdaFlowStep(Node nodeFrom, Node nodeTo, boolean preservesValue) { none() }
|
||||
|
||||
/**
|
||||
* Since our model is so simple, we dont want to compress the local flow steps.
|
||||
* This compression is normally done to not show SSA steps, casts, etc.
|
||||
*/
|
||||
predicate neverSkipInPathGraph(Node node) { any() }
|
||||
|
||||
predicate knownSourceModel(Node source, string model) { none() }
|
||||
|
||||
predicate knownSinkModel(Node sink, string model) { none() }
|
||||
@@ -1,194 +0,0 @@
|
||||
private import codeql.dataflow.DataFlow
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.actions.Cfg as Cfg
|
||||
private import codeql.Locations
|
||||
private import DataFlowPrivate
|
||||
|
||||
class Node extends TNode {
|
||||
/** Gets a textual representation of this element. */
|
||||
string toString() { none() }
|
||||
|
||||
Location getLocation() { none() }
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
this.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
AstNode asExpr() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Any Ast Expression.
|
||||
* UsesExpr, RunExpr, ArgumentExpr, VarAccessExpr, ...
|
||||
*/
|
||||
class ExprNode extends Node, TExprNode {
|
||||
private DataFlowExpr expr;
|
||||
|
||||
ExprNode() { this = TExprNode(expr) }
|
||||
|
||||
Cfg::Node getCfgNode() { result = expr }
|
||||
|
||||
override string toString() { result = expr.toString() }
|
||||
|
||||
override Location getLocation() { result = expr.getLocation() }
|
||||
|
||||
override AstNode asExpr() { result = expr.getAstNode() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable workflow input nodes
|
||||
*/
|
||||
class ParameterNode extends ExprNode {
|
||||
private Input input;
|
||||
|
||||
ParameterNode() { this.asExpr() = input }
|
||||
|
||||
predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
|
||||
input = c.(ReusableWorkflow).getInput(pos) or
|
||||
input = c.(CompositeAction).getInput(pos)
|
||||
}
|
||||
|
||||
override string toString() { result = "input " + input.toString() }
|
||||
|
||||
override Location getLocation() { result = input.getLocation() }
|
||||
|
||||
Input getInput() { result = input }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a data flow callable (Uses).
|
||||
*/
|
||||
class CallNode extends ExprNode {
|
||||
private DataFlowCall call;
|
||||
|
||||
CallNode() { this.getCfgNode() instanceof DataFlowCall }
|
||||
|
||||
DataFlowCallable getCalleeNode() { result = viableCallable(this.getCfgNode()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An argument to a Uses step (call).
|
||||
*/
|
||||
class ArgumentNode extends ExprNode {
|
||||
ArgumentNode() { this.getCfgNode().getAstNode() = any(Uses e).getArgumentExpr(_) }
|
||||
|
||||
predicate argumentOf(DataFlowCall call, ArgumentPosition pos) {
|
||||
this.getCfgNode() = call.(Cfg::Node).getASuccessor+() and
|
||||
call.(Cfg::Node).getAstNode() =
|
||||
any(Uses e | e.getArgumentExpr(pos) = this.getCfgNode().getAstNode())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable workflow output nodes
|
||||
*/
|
||||
class ReturnNode extends ExprNode {
|
||||
private Outputs outputs;
|
||||
|
||||
ReturnNode() {
|
||||
this.asExpr() = outputs and
|
||||
(
|
||||
exists(ReusableWorkflow w | w.getOutputs() = outputs) or
|
||||
exists(CompositeAction a | a.getOutputs() = outputs)
|
||||
)
|
||||
}
|
||||
|
||||
ReturnKind getKind() { result = TNormalReturn() }
|
||||
|
||||
override string toString() { result = "output " + outputs.toString() }
|
||||
|
||||
override Location getLocation() { result = outputs.getLocation() }
|
||||
}
|
||||
|
||||
/** Gets the node corresponding to `e`. */
|
||||
Node exprNode(DataFlowExpr e) { result = TExprNode(e) }
|
||||
|
||||
/**
|
||||
* An entity that represents a set of `Content`s.
|
||||
*
|
||||
* The set may be interpreted differently depending on whether it is
|
||||
* stored into (`getAStoreContent`) or read from (`getAReadContent`).
|
||||
*/
|
||||
class ContentSet instanceof Content {
|
||||
/** Gets a content that may be stored into when storing into this set. */
|
||||
Content getAStoreContent() { result = this }
|
||||
|
||||
/** Gets a content that may be read from when reading from this set. */
|
||||
Content getAReadContent() { result = this }
|
||||
|
||||
/** Gets a textual representation of this content set. */
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
super.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference contained in an object. Examples include instance fields, the
|
||||
* contents of a collection object, the contents of an array or pointer.
|
||||
*/
|
||||
class Content extends TContent {
|
||||
/** Gets the type of the contained data for the purpose of type pruning. */
|
||||
DataFlowType getType() { any() }
|
||||
|
||||
/** Gets a textual representation of this element. */
|
||||
abstract string toString();
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
filepath = "" and startline = 0 and startcolumn = 0 and endline = 0 and endcolumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** A field of an object, for example an instance variable. */
|
||||
class FieldContent extends Content, TFieldContent {
|
||||
private string name;
|
||||
|
||||
FieldContent() { this = TFieldContent(name) }
|
||||
|
||||
/** Gets the name of the field. */
|
||||
string getName() { result = name }
|
||||
|
||||
override string toString() { result = name }
|
||||
}
|
||||
|
||||
predicate hasLocalFlow(Node n1, Node n2) {
|
||||
n1 = n2 or
|
||||
simpleLocalFlowStep(n1, n2, _) or
|
||||
exists(ContentSet c | ctxFieldReadStep(n1, n2, c))
|
||||
}
|
||||
|
||||
predicate hasLocalFlowExpr(AstNode n1, AstNode n2) {
|
||||
exists(Node dn1, Node dn2 |
|
||||
dn1.asExpr() = n1 and
|
||||
dn2.asExpr() = n2 and
|
||||
hasLocalFlow(dn1, dn2)
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* This module provides extensible predicates for defining MaD models.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds if a source model exists for the given parameters.
|
||||
*/
|
||||
extensible predicate actionsSourceModel(
|
||||
string action, string version, string output, string kind, string provenance
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds if a summary model exists for the given parameters.
|
||||
*/
|
||||
extensible predicate actionsSummaryModel(
|
||||
string action, string version, string input, string output, string kind, string provenance
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds if a sink model exists for the given parameters.
|
||||
*/
|
||||
extensible predicate actionsSinkModel(
|
||||
string action, string version, string input, string kind, string provenance
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Provides Actions-specific definitions for use in the taint tracking library.
|
||||
* Implementation of https://github.com/github/codeql/blob/main/shared/dataflow/codeql/dataflow/TaintTracking.qll
|
||||
*/
|
||||
|
||||
private import codeql.Locations
|
||||
private import codeql.dataflow.TaintTracking
|
||||
private import DataFlowImplSpecific
|
||||
|
||||
module ActionsTaintTracking implements InputSig<Location, ActionsDataFlow> {
|
||||
import TaintTrackingPrivate
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Provides modules for performing local (intra-procedural) and
|
||||
* global (inter-procedural) taint-tracking analyses.
|
||||
*/
|
||||
|
||||
private import DataFlowPrivate
|
||||
private import codeql.actions.DataFlow
|
||||
private import codeql.actions.dataflow.TaintSteps
|
||||
private import codeql.actions.Ast
|
||||
|
||||
/**
|
||||
* Holds if `node` should be a sanitizer in all global taint flow configurations
|
||||
* but not in local taint.
|
||||
*/
|
||||
predicate defaultTaintSanitizer(DataFlow::Node node) { none() }
|
||||
|
||||
// predicate defaultAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// any(AdditionalTaintStep s).step(nodeFrom, nodeTo)
|
||||
// }
|
||||
/**
|
||||
* Holds if the additional step from `nodeFrom` to `nodeTo` should be included
|
||||
* in all global taint flow configurations.
|
||||
*/
|
||||
cached
|
||||
predicate defaultAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo, string model) {
|
||||
any(AdditionalTaintStep s).step(nodeFrom, nodeTo) and model = ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if taint flow configurations should allow implicit reads of `c` at sinks
|
||||
* and inputs to additional taint steps.
|
||||
*/
|
||||
bindingset[node]
|
||||
predicate defaultImplicitTaintRead(DataFlow::Node node, DataFlow::ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Holds if the additional step from `src` to `sink` should be considered in
|
||||
* speculative taint flow exploration.
|
||||
*/
|
||||
predicate speculativeTaintStep(DataFlow::Node src, DataFlow::Node sink) { none() }
|
||||
@@ -1,19 +0,0 @@
|
||||
private import codeql.files.FileSystem
|
||||
|
||||
/**
|
||||
* Returns an appropriately encoded version of a filename `name`
|
||||
* passed by the VS Code extension in order to coincide with the
|
||||
* output of `.getFile()` on locatable entities.
|
||||
*/
|
||||
cached
|
||||
File getFileBySourceArchiveName(string name) {
|
||||
// The name provided for a file in the source archive by the VS Code extension
|
||||
// has some differences from the absolute path in the database:
|
||||
// 1. colons are replaced by underscores
|
||||
// 2. there's a leading slash, even for Windows paths: "C:/foo/bar" ->
|
||||
// "/C_/foo/bar"
|
||||
// 3. double slashes in UNC prefixes are replaced with a single slash
|
||||
// We can handle 2 and 3 together by unconditionally adding a leading slash
|
||||
// before replacing double slashes.
|
||||
name = ("/" + result.getAbsolutePath().replaceAll(":", "_")).replaceAll("//", "/")
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* Provides queries to pretty-print an Kaleidoscope abstract syntax tree as a graph.
|
||||
*
|
||||
* By default, this will print the AST for all nodes in the database. To change
|
||||
* this behavior, extend `PrintASTConfiguration` and override `shouldPrintNode`
|
||||
* to hold for only the AST nodes you wish to view.
|
||||
*/
|
||||
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.Locations
|
||||
|
||||
/**
|
||||
* The query can extend this class to control which nodes are printed.
|
||||
*/
|
||||
class PrintAstConfiguration extends string {
|
||||
PrintAstConfiguration() { this = "PrintAstConfiguration" }
|
||||
|
||||
/**
|
||||
* Holds if the given node should be printed.
|
||||
*/
|
||||
predicate shouldPrintNode(PrintAstNode n) { any() }
|
||||
}
|
||||
|
||||
newtype TPrintNode = TPrintRegularAstNode(AstNode n) { any() }
|
||||
|
||||
private predicate shouldPrintNode(PrintAstNode n) {
|
||||
any(PrintAstConfiguration config).shouldPrintNode(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* A node in the output tree.
|
||||
*/
|
||||
class PrintAstNode extends TPrintNode {
|
||||
/** Gets a textual representation of this node in the PrintAst output tree. */
|
||||
string toString() { none() }
|
||||
|
||||
/**
|
||||
* Gets the child node with name `edgeName`. Typically this is the name of the
|
||||
* predicate used to access the child.
|
||||
*/
|
||||
PrintAstNode getChild(string edgeName) { none() }
|
||||
|
||||
/** Get the Location of this AST node */
|
||||
Location getLocation() { none() }
|
||||
|
||||
/** Gets a child of this node. */
|
||||
final PrintAstNode getAChild() { result = this.getChild(_) }
|
||||
|
||||
/** Gets the parent of this node, if any. */
|
||||
final PrintAstNode getParent() { result.getAChild() = this }
|
||||
|
||||
/** Gets a value used to order this node amongst its siblings. */
|
||||
int getOrder() {
|
||||
this =
|
||||
rank[result](PrintRegularAstNode p, Location l, File f |
|
||||
l = p.getLocation() and
|
||||
f = l.getFile()
|
||||
|
|
||||
p
|
||||
order by
|
||||
f.getBaseName(), f.getAbsolutePath(), l.getStartLine(), l.getStartColumn(),
|
||||
l.getEndLine(), l.getEndColumn()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the property of this node, where the name of the property
|
||||
* is `key`.
|
||||
*/
|
||||
final string getProperty(string key) {
|
||||
key = "semmle.label" and
|
||||
result = this.toString()
|
||||
or
|
||||
key = "semmle.order" and result = this.getOrder().toString()
|
||||
}
|
||||
}
|
||||
|
||||
/** An `AstNode` in the output tree. */
|
||||
class PrintRegularAstNode extends PrintAstNode, TPrintRegularAstNode {
|
||||
AstNode astNode;
|
||||
|
||||
PrintRegularAstNode() { this = TPrintRegularAstNode(astNode) }
|
||||
|
||||
override string toString() {
|
||||
result = "[" + concat(astNode.getAPrimaryQlClass(), ", ") + "] " + astNode.toString()
|
||||
}
|
||||
|
||||
override Location getLocation() { result = astNode.getLocation() }
|
||||
|
||||
override PrintAstNode getChild(string name) {
|
||||
exists(int i |
|
||||
name = i.toString() and
|
||||
result =
|
||||
TPrintRegularAstNode(rank[i](AstNode child, Location l |
|
||||
child.getParentNode() = astNode and
|
||||
child.getLocation() = l
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(),
|
||||
child.toString()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node` belongs to the output tree, and its property `key` has the
|
||||
* given `value`.
|
||||
*/
|
||||
query predicate nodes(PrintAstNode node, string key, string value) {
|
||||
value = node.getProperty(key) and shouldPrintNode(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `target` is a child of `source` in the AST, and property `key` of
|
||||
* the edge has the given `value`.
|
||||
*/
|
||||
query predicate edges(PrintAstNode source, PrintAstNode target, string key, string value) {
|
||||
shouldPrintNode(source) and
|
||||
shouldPrintNode(target) and
|
||||
target = source.getChild(_) and
|
||||
(
|
||||
key = "semmle.label" and
|
||||
value = strictconcat(string name | source.getChild(name) = target | name, "/")
|
||||
or
|
||||
key = "semmle.order" and
|
||||
value = target.getProperty("semmle.order")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if property `key` of the graph has the given `value`.
|
||||
*/
|
||||
query predicate graphProperties(string key, string value) {
|
||||
key = "semmle.graphKind" and value = "tree"
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.DataFlow
|
||||
|
||||
abstract class ArgumentInjectionSink extends DataFlow::Node {
|
||||
abstract string getCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it as the argument to a command vulnerable to argument injection.
|
||||
* e.g.
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* sed "s/FOO/$BODY/g" > /tmp/foo
|
||||
*/
|
||||
class ArgumentInjectionFromEnvVarSink extends ArgumentInjectionSink {
|
||||
string command;
|
||||
string argument;
|
||||
|
||||
ArgumentInjectionFromEnvVarSink() {
|
||||
exists(Run run, string var |
|
||||
run.getScript() = this.asExpr() and
|
||||
(
|
||||
exists(run.getInScopeEnvVarExpr(var)) or
|
||||
var = "GITHUB_HEAD_REF"
|
||||
) and
|
||||
run.getScript().getAnEnvReachingArgumentInjectionSink(var, command, argument)
|
||||
)
|
||||
}
|
||||
|
||||
override string getCommand() { result = command }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to an unsafe argument
|
||||
* e.g.
|
||||
* run: |
|
||||
* BODY=$(git log --format=%s)
|
||||
* sed "s/FOO/$BODY/g" > /tmp/foo
|
||||
*/
|
||||
class ArgumentInjectionFromCommandSink extends ArgumentInjectionSink {
|
||||
string command;
|
||||
string argument;
|
||||
|
||||
ArgumentInjectionFromCommandSink() {
|
||||
exists(CommandSource source, Run run |
|
||||
run = source.getEnclosingRun() and
|
||||
this.asExpr() = run.getScript() and
|
||||
run.getScript().getACmdReachingArgumentInjectionSink(source.getCommand(), command, argument)
|
||||
)
|
||||
}
|
||||
|
||||
override string getCommand() { result = command }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it as the argument to a command vulnerable to argument injection.
|
||||
*/
|
||||
class ArgumentInjectionFromMaDSink extends ArgumentInjectionSink {
|
||||
ArgumentInjectionFromMaDSink() { madSink(this, "argument-injection") }
|
||||
|
||||
override string getCommand() { result = "unknown" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a code script.
|
||||
*/
|
||||
private module ArgumentInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RemoteFlowSource
|
||||
or
|
||||
exists(Run run |
|
||||
run.getScript() = source.asExpr() and
|
||||
run.getScript().getAnEnvReachingArgumentInjectionSink("GITHUB_HEAD_REF", _, _)
|
||||
)
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof ArgumentInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScript() and
|
||||
run.getScript().getAnEnvReachingArgumentInjectionSink(var, _, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */
|
||||
module ArgumentInjectionFlow = TaintTracking::Global<ArgumentInjectionConfig>;
|
||||
@@ -1,322 +0,0 @@
|
||||
import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
import codeql.actions.DataFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.security.PoisonableSteps
|
||||
import codeql.actions.security.UntrustedCheckoutQuery
|
||||
|
||||
string unzipRegexp() { result = "(unzip|tar)\\s+.*" }
|
||||
|
||||
string unzipDirArgRegexp() { result = "(-d|-C)\\s+([^ ]+).*" }
|
||||
|
||||
abstract class UntrustedArtifactDownloadStep extends Step {
|
||||
abstract string getPath();
|
||||
}
|
||||
|
||||
class GitHubDownloadArtifactActionStep extends UntrustedArtifactDownloadStep, UsesStep {
|
||||
GitHubDownloadArtifactActionStep() {
|
||||
this.getCallee() = "actions/download-artifact" and
|
||||
(
|
||||
// By default, the permissions are scoped so they can only download Artifacts within the current workflow run.
|
||||
// To elevate permissions for this scenario, you can specify a github-token along with other repository and run identifiers
|
||||
this.getArgument("run-id").matches("%github.event.workflow_run.id%") and
|
||||
exists(this.getArgument("github-token"))
|
||||
or
|
||||
// There is an artifact upload step in the same workflow which can be influenced by an attacker on a checkout step
|
||||
exists(LocalJob job, SimplePRHeadCheckoutStep checkout, UsesStep upload |
|
||||
this.getEnclosingWorkflow().getAJob() = job and
|
||||
job.getAStep() = checkout and
|
||||
checkout.getATriggerEvent().getName() = "pull_request_target" and
|
||||
checkout.getAFollowingStep() = upload and
|
||||
upload.getCallee() = "actions/upload-artifact"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if exists(this.getArgument("path"))
|
||||
then result = normalizePath(this.getArgument("path"))
|
||||
else result = "GITHUB_WORKSPACE/"
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadArtifactActionStep extends UntrustedArtifactDownloadStep, UsesStep {
|
||||
DownloadArtifactActionStep() {
|
||||
this.getCallee() =
|
||||
[
|
||||
"dawidd6/action-download-artifact", "marcofaggian/action-download-multiple-artifacts",
|
||||
"benday-inc/download-latest-artifact", "blablacar/action-download-last-artifact",
|
||||
"levonet/action-download-last-artifact", "bettermarks/action-artifact-download",
|
||||
"aochmann/actions-download-artifact", "cytopia/download-artifact-retry-action",
|
||||
"alextompkins/download-prior-artifact", "nmerget/download-gzip-artifact",
|
||||
"benday-inc/download-artifact", "synergy-au/download-workflow-artifacts-action",
|
||||
"ishworkh/docker-image-artifact-download", "ishworkh/container-image-artifact-download",
|
||||
"sidx1024/action-download-artifact", "hyperskill/azblob-download-artifact",
|
||||
"ma-ve/action-download-artifact-with-retry"
|
||||
] and
|
||||
(
|
||||
not exists(this.getArgument(["branch", "branch_name"]))
|
||||
or
|
||||
exists(this.getArgument(["branch", "branch_name"])) and
|
||||
this.getArgument("allow_forks") = "true"
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument(["commit", "commitHash", "commit_sha"])) or
|
||||
not this.getArgument(["commit", "commitHash", "commit_sha"])
|
||||
.matches("%github.event.pull_request.head.sha%")
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("event")) or
|
||||
not this.getArgument("event") = "pull_request"
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument(["run-id", "run_id", "workflow-run-id", "workflow_run_id"])) or
|
||||
this.getArgument(["run-id", "run_id", "workflow-run-id", "workflow_run_id"])
|
||||
.matches("%github.event.workflow_run.id%")
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("pr")) or
|
||||
not this.getArgument("pr")
|
||||
.matches(["%github.event.pull_request.number%", "%github.event.number%"])
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if exists(this.getArgument(["path", "download_path"]))
|
||||
then result = normalizePath(this.getArgument(["path", "download_path"]))
|
||||
else
|
||||
if exists(this.getArgument("paths"))
|
||||
then result = normalizePath(this.getArgument("paths").splitAt(" "))
|
||||
else result = "GITHUB_WORKSPACE/"
|
||||
}
|
||||
}
|
||||
|
||||
class LegitLabsDownloadArtifactActionStep extends UntrustedArtifactDownloadStep, UsesStep {
|
||||
LegitLabsDownloadArtifactActionStep() {
|
||||
this.getCallee() = "Legit-Labs/action-download-artifact" and
|
||||
(
|
||||
not exists(this.getArgument("branch")) or
|
||||
not this.getArgument("branch") = ["main", "master"]
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("commit")) or
|
||||
not this.getArgument("commit").matches("%github.event.pull_request.head.sha%")
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("event")) or
|
||||
not this.getArgument("event") = "pull_request"
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("run_id")) or
|
||||
not this.getArgument("run_id").matches("%github.event.workflow_run.id%")
|
||||
) and
|
||||
(
|
||||
not exists(this.getArgument("pr")) or
|
||||
not this.getArgument("pr").matches("%github.event.pull_request.number%")
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if exists(this.getArgument("path"))
|
||||
then result = normalizePath(this.getArgument("path"))
|
||||
else result = "GITHUB_WORKSPACE/artifacts"
|
||||
}
|
||||
}
|
||||
|
||||
class ActionsGitHubScriptDownloadStep extends UntrustedArtifactDownloadStep, UsesStep {
|
||||
string script;
|
||||
|
||||
ActionsGitHubScriptDownloadStep() {
|
||||
// eg:
|
||||
// - uses: actions/github-script@v6
|
||||
// with:
|
||||
// script: |
|
||||
// let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
// owner: context.repo.owner,
|
||||
// repo: context.repo.repo,
|
||||
// run_id: context.payload.workflow_run.id,
|
||||
// });
|
||||
// let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
// return artifact.name == "<ARTEFACT_NAME>"
|
||||
// })[0];
|
||||
// let 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}}/test-results.zip', Buffer.from(download.data));
|
||||
this.getCallee() = "actions/github-script" and
|
||||
this.getArgument("script") = script and
|
||||
script.matches("%listWorkflowRunArtifacts(%") and
|
||||
script.matches("%downloadArtifact(%") and
|
||||
script.matches("%writeFileSync(%") and
|
||||
// Filter out artifacts that were created by pull-request.
|
||||
not script.matches("%exclude_pull_requests: true%")
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if
|
||||
this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpMatch(unzipRegexp() + unzipDirArgRegexp())
|
||||
then
|
||||
result =
|
||||
normalizePath(trimQuotes(this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 3)))
|
||||
else
|
||||
if this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp())
|
||||
then result = "GITHUB_WORKSPACE/"
|
||||
else none()
|
||||
}
|
||||
}
|
||||
|
||||
class GHRunArtifactDownloadStep extends UntrustedArtifactDownloadStep, Run {
|
||||
GHRunArtifactDownloadStep() {
|
||||
// eg: - run: gh run download ${{ github.event.workflow_run.id }} --repo "${GITHUB_REPOSITORY}" --name "artifact_name"
|
||||
this.getScript().getACommand().regexpMatch(".*gh\\s+run\\s+download.*") and
|
||||
(
|
||||
this.getScript().getACommand().regexpMatch(unzipRegexp()) or
|
||||
this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp())
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if
|
||||
this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or
|
||||
this.getScript().getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp())
|
||||
then
|
||||
result =
|
||||
normalizePath(trimQuotes(this.getScript()
|
||||
.getACommand()
|
||||
.regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 3))) or
|
||||
result =
|
||||
normalizePath(trimQuotes(this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 3)))
|
||||
else
|
||||
if
|
||||
this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp()) or
|
||||
this.getScript().getACommand().regexpMatch(unzipRegexp())
|
||||
then result = "GITHUB_WORKSPACE/"
|
||||
else none()
|
||||
}
|
||||
}
|
||||
|
||||
class DirectArtifactDownloadStep extends UntrustedArtifactDownloadStep, Run {
|
||||
DirectArtifactDownloadStep() {
|
||||
// eg:
|
||||
// run: |
|
||||
// artifacts_url=${{ github.event.workflow_run.artifacts_url }}
|
||||
// gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact
|
||||
// do
|
||||
// IFS=$'\t' read name url <<< "$artifact"
|
||||
// gh api $url > "$name.zip"
|
||||
// unzip -d "$name" "$name.zip"
|
||||
// done
|
||||
this.getScript().getACommand().matches("%github.event.workflow_run.artifacts_url%") and
|
||||
(
|
||||
this.getScript().getACommand().regexpMatch(unzipRegexp()) or
|
||||
this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp())
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if
|
||||
this.getScript().getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or
|
||||
this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpMatch(unzipRegexp() + unzipDirArgRegexp())
|
||||
then
|
||||
result =
|
||||
normalizePath(trimQuotes(this.getScript()
|
||||
.getACommand()
|
||||
.regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 3))) or
|
||||
result =
|
||||
normalizePath(trimQuotes(this.getAFollowingStep()
|
||||
.(Run)
|
||||
.getScript()
|
||||
.getACommand()
|
||||
.regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 3)))
|
||||
else result = "GITHUB_WORKSPACE/"
|
||||
}
|
||||
}
|
||||
|
||||
class ArtifactPoisoningSink extends DataFlow::Node {
|
||||
UntrustedArtifactDownloadStep download;
|
||||
PoisonableStep poisonable;
|
||||
|
||||
ArtifactPoisoningSink() {
|
||||
download.getAFollowingStep() = poisonable and
|
||||
// excluding artifacts downloaded to /tmp
|
||||
not download.getPath().regexpMatch("^/tmp.*") and
|
||||
(
|
||||
poisonable.(Run).getScript() = this.asExpr() and
|
||||
(
|
||||
// Check if the poisonable step is a local script execution step
|
||||
// and the path of the command or script matches the path of the downloaded artifact
|
||||
isSubpath(poisonable.(LocalScriptExecutionRunStep).getPath(), download.getPath())
|
||||
or
|
||||
// Checking the path for non local script execution steps is very difficult
|
||||
not poisonable instanceof LocalScriptExecutionRunStep
|
||||
// Its not easy to extract the path from a non-local script execution step so skipping this check for now
|
||||
// and isSubpath(poisonable.(Run).getWorkingDirectory(), download.getPath())
|
||||
)
|
||||
or
|
||||
poisonable.(UsesStep) = this.asExpr() and
|
||||
(
|
||||
not poisonable instanceof LocalActionUsesStep and
|
||||
download.getPath() = "GITHUB_WORKSPACE/"
|
||||
or
|
||||
isSubpath(poisonable.(LocalActionUsesStep).getPath(), download.getPath())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
string getPath() { result = download.getPath() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe artifacts
|
||||
* that is used may lead to artifact poisoning
|
||||
*/
|
||||
private module ArtifactPoisoningConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof ArtifactSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof ArtifactPoisoningSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(PoisonableStep step |
|
||||
pred instanceof ArtifactSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
(
|
||||
succ.asExpr() = step.(Run).getScript() or
|
||||
succ.asExpr() = step.(UsesStep)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof ArtifactSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe artifacts that is used in an insecure way. */
|
||||
module ArtifactPoisoningFlow = TaintTracking::Global<ArtifactPoisoningConfig>;
|
||||
@@ -1,72 +0,0 @@
|
||||
import actions
|
||||
|
||||
string defaultBranchTriggerEvent() {
|
||||
result =
|
||||
[
|
||||
"check_run", "check_suite", "delete", "discussion", "discussion_comment", "fork", "gollum",
|
||||
"issue_comment", "issues", "label", "milestone", "project", "project_card", "project_column",
|
||||
"public", "pull_request_comment", "pull_request_target", "repository_dispatch", "schedule",
|
||||
"watch", "workflow_run"
|
||||
]
|
||||
}
|
||||
|
||||
predicate runsOnDefaultBranch(Event e) {
|
||||
(
|
||||
e.getName() = defaultBranchTriggerEvent() and
|
||||
not e.getName() = "pull_request_target"
|
||||
or
|
||||
e.getName() = "push" and
|
||||
e.getAPropertyValue("branches") = defaultBranchNames()
|
||||
or
|
||||
e.getName() = "pull_request_target" and
|
||||
(
|
||||
// no filtering
|
||||
not e.hasProperty("branches") and not e.hasProperty("branches-ignore")
|
||||
or
|
||||
// only branches-ignore filter
|
||||
e.hasProperty("branches-ignore") and
|
||||
not e.hasProperty("branches") and
|
||||
not e.getAPropertyValue("branches-ignore") = defaultBranchNames()
|
||||
or
|
||||
// only branches filter
|
||||
e.hasProperty("branches") and
|
||||
not e.hasProperty("branches-ignore") and
|
||||
e.getAPropertyValue("branches") = defaultBranchNames()
|
||||
or
|
||||
// branches and branches-ignore filters
|
||||
e.hasProperty("branches") and
|
||||
e.hasProperty("branches-ignore") and
|
||||
e.getAPropertyValue("branches") = defaultBranchNames() and
|
||||
not e.getAPropertyValue("branches-ignore") = defaultBranchNames()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
abstract class CacheWritingStep extends Step {
|
||||
abstract string getPath();
|
||||
}
|
||||
|
||||
class CacheActionUsesStep extends CacheWritingStep, UsesStep {
|
||||
CacheActionUsesStep() { this.getCallee() = "actions/cache" }
|
||||
|
||||
override string getPath() {
|
||||
result = normalizePath(this.(UsesStep).getArgument("path").splitAt("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
class CacheActionSaveUsesStep extends CacheWritingStep, UsesStep {
|
||||
CacheActionSaveUsesStep() { this.getCallee() = "actions/cache/save" }
|
||||
|
||||
override string getPath() {
|
||||
result = normalizePath(this.(UsesStep).getArgument("path").splitAt("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
class SetupRubyUsesStep extends CacheWritingStep, UsesStep {
|
||||
SetupRubyUsesStep() {
|
||||
this.getCallee() = ["actions/setup-ruby", "ruby/setup-ruby"] and
|
||||
this.getArgument("bundler-cache") = "true"
|
||||
}
|
||||
|
||||
override string getPath() { result = normalizePath("vendor/bundle") }
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.DataFlow
|
||||
|
||||
class CodeInjectionSink extends DataFlow::Node {
|
||||
CodeInjectionSink() {
|
||||
exists(Run e | e.getAnScriptExpr() = this.asExpr()) or
|
||||
madSink(this, "code-injection")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a code script.
|
||||
*/
|
||||
private module CodeInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "code-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */
|
||||
module CodeInjectionFlow = TaintTracking::Global<CodeInjectionConfig>;
|
||||
@@ -1,22 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.DataFlow
|
||||
|
||||
private class CommandInjectionSink extends DataFlow::Node {
|
||||
CommandInjectionSink() { madSink(this, "command-injection") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a system command.
|
||||
*/
|
||||
private module CommandInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof CommandInjectionSink }
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a system command. */
|
||||
module CommandInjectionFlow = TaintTracking::Global<CommandInjectionConfig>;
|
||||
@@ -1,312 +0,0 @@
|
||||
import actions
|
||||
|
||||
string any_category() {
|
||||
result =
|
||||
[
|
||||
"untrusted-checkout", "output-clobbering", "envpath-injection", "envvar-injection",
|
||||
"command-injection", "argument-injection", "code-injection", "cache-poisoning",
|
||||
"untrusted-checkout-toctou", "artifact-poisoning", "artifact-poisoning-toctou"
|
||||
]
|
||||
}
|
||||
|
||||
string non_toctou_category() {
|
||||
result = any_category() and not result = "untrusted-checkout-toctou"
|
||||
}
|
||||
|
||||
string toctou_category() { result = ["untrusted-checkout-toctou", "artifact-poisoning-toctou"] }
|
||||
|
||||
string any_event() { result = actor_not_attacker_event() or result = actor_is_attacker_event() }
|
||||
|
||||
string actor_is_attacker_event() {
|
||||
result =
|
||||
[
|
||||
// actor and attacker have to be the same
|
||||
"pull_request_target",
|
||||
"workflow_run",
|
||||
"discussion_comment",
|
||||
"discussion",
|
||||
"issues",
|
||||
"fork",
|
||||
"watch"
|
||||
]
|
||||
}
|
||||
|
||||
string actor_not_attacker_event() {
|
||||
result =
|
||||
[
|
||||
// actor and attacker can be different
|
||||
// actor may be a collaborator, but the attacker is may be the author of the PR that gets commented
|
||||
// therefore it may be vulnerable to TOCTOU races where the actor reviews one thing and the attacker changes it
|
||||
"issue_comment",
|
||||
"pull_request_comment",
|
||||
]
|
||||
}
|
||||
|
||||
/** An If node that contains an actor, user or label check */
|
||||
abstract class ControlCheck extends AstNode {
|
||||
ControlCheck() {
|
||||
this instanceof If or
|
||||
this instanceof Environment or
|
||||
this instanceof UsesStep or
|
||||
this instanceof Run
|
||||
}
|
||||
|
||||
predicate protects(AstNode node, Event event, string category) {
|
||||
// The check dominates the step it should protect
|
||||
this.dominates(node) and
|
||||
// The check is effective against the event and category
|
||||
this.protectsCategoryAndEvent(category, event.getName()) and
|
||||
// The check can be triggered by the event
|
||||
this.getATriggerEvent() = event
|
||||
}
|
||||
|
||||
predicate dominates(AstNode node) {
|
||||
this instanceof If and
|
||||
(
|
||||
node.getEnclosingStep().getIf() = this or
|
||||
node.getEnclosingJob().getIf() = this or
|
||||
node.getEnclosingJob().getANeededJob().(LocalJob).getAStep().getIf() = this or
|
||||
node.getEnclosingJob().getANeededJob().(LocalJob).getIf() = this
|
||||
)
|
||||
or
|
||||
this instanceof Environment and
|
||||
(
|
||||
node.getEnclosingJob().getEnvironment() = this
|
||||
or
|
||||
node.getEnclosingJob().getANeededJob().getEnvironment() = this
|
||||
)
|
||||
or
|
||||
(
|
||||
this instanceof Run or
|
||||
this instanceof UsesStep
|
||||
) and
|
||||
(
|
||||
this.(Step).getAFollowingStep() = node.getEnclosingStep()
|
||||
or
|
||||
node.getEnclosingJob().getANeededJob().(LocalJob).getAStep() = this.(Step)
|
||||
)
|
||||
}
|
||||
|
||||
abstract predicate protectsCategoryAndEvent(string category, string event);
|
||||
}
|
||||
|
||||
abstract class AssociationCheck extends ControlCheck {
|
||||
// Checks if the actor is a MEMBER/OWNER the repo
|
||||
// - they are effective against pull requests and workflow_run (since these are triggered by pull_requests) since they can control who is making the PR
|
||||
// - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = actor_is_attacker_event() and category = any_category()
|
||||
or
|
||||
event = actor_not_attacker_event() and category = non_toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ActorCheck extends ControlCheck {
|
||||
// checks for a specific actor
|
||||
// - they are effective against pull requests and workflow_run (since these are triggered by pull_requests) since they can control who is making the PR
|
||||
// - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = actor_is_attacker_event() and category = any_category()
|
||||
or
|
||||
event = actor_not_attacker_event() and category = non_toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RepositoryCheck extends ControlCheck {
|
||||
// checks that the origin of the code is the same as the repository.
|
||||
// for pull_requests, that means that it triggers only on local branches or repos from the same org
|
||||
// - they are effective against pull requests/workflow_run since they can control where the code is coming from
|
||||
// - they are not effective against issue_comment since the repository will always be the same
|
||||
}
|
||||
|
||||
abstract class PermissionCheck extends ControlCheck {
|
||||
// checks that the actor has a specific permission level
|
||||
// - they are effective against pull requests/workflow_run since they can control who can make changes
|
||||
// - they are not effective against issue_comment since the author of the comment may not be the same as the author of the PR
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = actor_is_attacker_event() and category = any_category()
|
||||
or
|
||||
event = actor_not_attacker_event() and category = non_toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LabelCheck extends ControlCheck {
|
||||
// checks if the issue/pull_request is labeled, which implies that it could have been approved
|
||||
// - they dont protect against mutation attacks
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = actor_is_attacker_event() and category = any_category()
|
||||
or
|
||||
event = actor_not_attacker_event() and category = non_toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
class EnvironmentCheck extends ControlCheck instanceof Environment {
|
||||
// Environment checks are not effective against any mutable attacks
|
||||
// they do actually protect against untrusted code execution (sha)
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = actor_is_attacker_event() and category = any_category()
|
||||
or
|
||||
event = actor_not_attacker_event() and category = non_toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CommentVsHeadDateCheck extends ControlCheck {
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
// by itself, this check is not effective against any attacks
|
||||
event = actor_not_attacker_event() and category = toctou_category()
|
||||
}
|
||||
}
|
||||
|
||||
/* Specific implementations of control checks */
|
||||
class LabelIfCheck extends LabelCheck instanceof If {
|
||||
string condition;
|
||||
|
||||
LabelIfCheck() {
|
||||
condition = normalizeExpr(this.getCondition()) and
|
||||
(
|
||||
// eg: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
condition.regexpMatch(".*(^|[^!])contains\\(\\s*github\\.event\\.pull_request\\.labels\\b.*")
|
||||
or
|
||||
// eg: github.event.label.name == 'safe to test'
|
||||
condition.regexpMatch(".*\\bgithub\\.event\\.label\\.name\\s*==.*")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ActorIfCheck extends ActorCheck instanceof If {
|
||||
ActorIfCheck() {
|
||||
// eg: github.event.pull_request.user.login == 'admin'
|
||||
exists(
|
||||
normalizeExpr(this.getCondition())
|
||||
.regexpFind([
|
||||
"\\bgithub\\.event\\.pull_request\\.user\\.login\\b",
|
||||
"\\bgithub\\.event\\.head_commit\\.author\\.name\\b",
|
||||
"\\bgithub\\.event\\.commits.*\\.author\\.name\\b",
|
||||
"\\bgithub\\.event\\.sender\\.login\\b"
|
||||
], _, _)
|
||||
)
|
||||
or
|
||||
// eg: github.actor == 'admin'
|
||||
// eg: github.triggering_actor == 'admin'
|
||||
exists(
|
||||
normalizeExpr(this.getCondition())
|
||||
.regexpFind(["\\bgithub\\.actor\\b", "\\bgithub\\.triggering_actor\\b",], _, _)
|
||||
) and
|
||||
not normalizeExpr(this.getCondition()).matches("%[bot]%")
|
||||
}
|
||||
}
|
||||
|
||||
class PullRequestTargetRepositoryIfCheck extends RepositoryCheck instanceof If {
|
||||
PullRequestTargetRepositoryIfCheck() {
|
||||
// eg: github.event.pull_request.head.repo.full_name == github.repository
|
||||
exists(
|
||||
normalizeExpr(this.getCondition())
|
||||
// github.repository in a workflow_run event triggered by a pull request is the base repository
|
||||
.regexpFind([
|
||||
"\\bgithub\\.repository\\b", "\\bgithub\\.repository_owner\\b",
|
||||
"\\bgithub\\.event\\.pull_request\\.head\\.repo\\.full_name\\b",
|
||||
"\\bgithub\\.event\\.pull_request\\.head\\.repo\\.owner\\.name\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_repository\\.full_name\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_repository\\.owner\\.name\\b"
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = "pull_request_target" and category = any_category()
|
||||
}
|
||||
}
|
||||
|
||||
class WorkflowRunRepositoryIfCheck extends RepositoryCheck instanceof If {
|
||||
WorkflowRunRepositoryIfCheck() {
|
||||
// eg: github.event.workflow_run.head_repository.full_name == github.repository
|
||||
exists(
|
||||
normalizeExpr(this.getCondition())
|
||||
// github.repository in a workflow_run event triggered by a pull request is the base repository
|
||||
.regexpFind([
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_repository\\.full_name\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_repository\\.owner\\.name\\b"
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate protectsCategoryAndEvent(string category, string event) {
|
||||
event = "workflow_run" and category = any_category()
|
||||
}
|
||||
}
|
||||
|
||||
class AssociationIfCheck extends AssociationCheck instanceof If {
|
||||
AssociationIfCheck() {
|
||||
// eg: contains(fromJson('["MEMBER", "OWNER"]'), github.event.comment.author_association)
|
||||
normalizeExpr(this.getCondition())
|
||||
.splitAt("\n")
|
||||
.regexpMatch([
|
||||
".*\\bgithub\\.event\\.comment\\.author_association\\b.*",
|
||||
".*\\bgithub\\.event\\.issue\\.author_association\\b.*",
|
||||
".*\\bgithub\\.event\\.pull_request\\.author_association\\b.*",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class AssociationActionCheck extends AssociationCheck instanceof UsesStep {
|
||||
AssociationActionCheck() {
|
||||
this.getCallee() = "TheModdingInquisition/actions-team-membership" and
|
||||
(
|
||||
not exists(this.getArgument("exit"))
|
||||
or
|
||||
this.getArgument("exit") = "true"
|
||||
)
|
||||
or
|
||||
this.getCallee() = "actions/github-script" and
|
||||
this.getArgument("script").splitAt("\n").matches("%getMembershipForUserInOrg%")
|
||||
or
|
||||
this.getCallee() = "octokit/request-action" and
|
||||
this.getArgument("route").regexpMatch("GET.*(memberships).*")
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionActionCheck extends PermissionCheck instanceof UsesStep {
|
||||
PermissionActionCheck() {
|
||||
this.getCallee() = "actions-cool/check-user-permission" and
|
||||
(
|
||||
// default permission level is write
|
||||
not exists(this.getArgument("permission-level")) or
|
||||
this.getArgument("require") = ["write", "admin"]
|
||||
)
|
||||
or
|
||||
this.getCallee() = "sushichop/action-repository-permission" and
|
||||
this.getArgument("required-permission") = ["write", "admin"]
|
||||
or
|
||||
this.getCallee() = "prince-chrismc/check-actor-permissions-action" and
|
||||
this.getArgument("permission") = ["write", "admin"]
|
||||
or
|
||||
this.getCallee() = "lannonbr/repo-permission-check-action" and
|
||||
this.getArgument("permission") = ["write", "admin"]
|
||||
or
|
||||
this.getCallee() = "xt0rted/slash-command-action" and
|
||||
(
|
||||
// default permission level is write
|
||||
not exists(this.getArgument("permission-level")) or
|
||||
this.getArgument("permission-level") = ["write", "admin"]
|
||||
)
|
||||
or
|
||||
this.getCallee() = "actions/github-script" and
|
||||
this.getArgument("script").splitAt("\n").matches("%getCollaboratorPermissionLevel%")
|
||||
or
|
||||
this.getCallee() = "octokit/request-action" and
|
||||
this.getArgument("route").regexpMatch("GET.*(collaborators|permission).*")
|
||||
}
|
||||
}
|
||||
|
||||
class BashCommentVsHeadDateCheck extends CommentVsHeadDateCheck, Run {
|
||||
BashCommentVsHeadDateCheck() {
|
||||
// eg: if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then
|
||||
exists(string cmd1, string cmd2 |
|
||||
cmd1 = this.getScript().getACommand() and
|
||||
cmd2 = this.getScript().getACommand() and
|
||||
not cmd1 = cmd2 and
|
||||
cmd1.toLowerCase().regexpMatch("date\\s+-d.*(commit|pushed|comment|commented)_at.*") and
|
||||
cmd2.toLowerCase().regexpMatch("date\\s+-d.*(commit|pushed|comment|commented)_at.*")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
private import codeql.actions.security.ArtifactPoisoningQuery
|
||||
private import codeql.actions.security.UntrustedCheckoutQuery
|
||||
|
||||
abstract class EnvPathInjectionSink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares a PATH environment variable with contents from a local file.
|
||||
*/
|
||||
class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromFileReadSink() {
|
||||
exists(Run run, Step step |
|
||||
(
|
||||
step instanceof UntrustedArtifactDownloadStep or
|
||||
step instanceof PRHeadCheckoutStep
|
||||
) and
|
||||
this.asExpr() = run.getScript() and
|
||||
step.getAFollowingStep() = run and
|
||||
(
|
||||
// echo "$(cat foo.txt)" >> $GITHUB_PATH
|
||||
// FOO=$(cat foo.txt)
|
||||
// echo "$FOO" >> $GITHUB_PATH
|
||||
exists(string cmd |
|
||||
run.getScript().getAFileReadCommand() = cmd and
|
||||
run.getScript().getACmdReachingGitHubPathWrite(cmd)
|
||||
)
|
||||
or
|
||||
// cat foo.txt >> $GITHUB_PATH
|
||||
run.getScript().fileToGitHubPath(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to GITHUB_ENV
|
||||
* e.g.
|
||||
* run: |
|
||||
* COMMIT_MESSAGE=$(git log --format=%s)
|
||||
* echo "${COMMIT_MESSAGE}" >> $GITHUB_PATH
|
||||
*/
|
||||
class EnvPathInjectionFromCommandSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromCommandSink() {
|
||||
exists(CommandSource source |
|
||||
this.asExpr() = source.getEnclosingRun().getScript() and
|
||||
source.getEnclosingRun().getScript().getACmdReachingGitHubPathWrite(source.getCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it to declare a PATH env var.
|
||||
* e.g.
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* echo "$BODY" >> $GITHUB_PATH
|
||||
*/
|
||||
class EnvPathInjectionFromEnvVarSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromEnvVarSink() {
|
||||
exists(Run run, string var_name |
|
||||
run.getScript().getAnEnvReachingGitHubPathWrite(var_name) and
|
||||
exists(run.getInScopeEnvVarExpr(var_name)) and
|
||||
run.getScript() = this.asExpr()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EnvPathInjectionFromMaDSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromMaDSink() { madSink(this, "envpath-injection") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate an environment variable.
|
||||
*/
|
||||
private module EnvPathInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof EnvPathInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScript() and
|
||||
(
|
||||
run.getScript().getAnEnvReachingGitHubEnvWrite(var, _)
|
||||
or
|
||||
run.getScript().getAnEnvReachingGitHubOutputWrite(var, _)
|
||||
or
|
||||
run.getScript().getAnEnvReachingGitHubPathWrite(var)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "envpath-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate the PATH environment variable. */
|
||||
module EnvPathInjectionFlow = TaintTracking::Global<EnvPathInjectionConfig>;
|
||||
@@ -1,169 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
private import codeql.actions.security.ArtifactPoisoningQuery
|
||||
private import codeql.actions.security.UntrustedCheckoutQuery
|
||||
|
||||
abstract class EnvVarInjectionSink extends DataFlow::Node { }
|
||||
|
||||
string sanitizerCommand() {
|
||||
result =
|
||||
[
|
||||
"tr\\s+(-d\\s*)?('|\")?.n('|\")?", // tr -d '\n' ' ', tr '\n' ' '
|
||||
"tr\\s+-cd\\s+.*:al(pha|num):", // tr -cd '[:alpha:_]'
|
||||
"(head|tail)\\s+-n\\s+1" // head -n 1, tail -n 1
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable with contents from a local file.
|
||||
*/
|
||||
class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
|
||||
EnvVarInjectionFromFileReadSink() {
|
||||
exists(Run run, Step step |
|
||||
(
|
||||
step instanceof UntrustedArtifactDownloadStep or
|
||||
step instanceof PRHeadCheckoutStep
|
||||
) and
|
||||
this.asExpr() = run.getScript() and
|
||||
step.getAFollowingStep() = run and
|
||||
(
|
||||
// eg:
|
||||
// echo "SHA=$(cat test-results/sha-number)" >> $GITHUB_ENV
|
||||
// echo "SHA=$(<test-results/sha-number)" >> $GITHUB_ENV
|
||||
// FOO=$(cat test-results/sha-number)
|
||||
// echo "FOO=$FOO" >> $GITHUB_ENV
|
||||
exists(string cmd, string var, string sanitizer |
|
||||
run.getScript().getAFileReadCommand() = cmd and
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(cmd, var) and
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(sanitizer, var) and
|
||||
not exists(sanitizer.regexpFind(sanitizerCommand(), _, _))
|
||||
)
|
||||
or
|
||||
// eg: cat test-results/.env >> $GITHUB_ENV
|
||||
run.getScript().fileToGitHubEnv(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to GITHUB_ENV
|
||||
* e.g.
|
||||
* run: |
|
||||
* COMMIT_MESSAGE=$(git log --format=%s)
|
||||
* echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV
|
||||
*/
|
||||
class EnvVarInjectionFromCommandSink extends EnvVarInjectionSink {
|
||||
CommandSource inCommand;
|
||||
string injectedVar;
|
||||
string command;
|
||||
|
||||
EnvVarInjectionFromCommandSink() {
|
||||
exists(Run run |
|
||||
this.asExpr() = inCommand.getEnclosingRun().getScript() and
|
||||
run = inCommand.getEnclosingRun() and
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(inCommand.getCommand(), injectedVar) and
|
||||
(
|
||||
// the source flows to the injected variable without any command in between
|
||||
not run.getScript().getACmdReachingGitHubEnvWrite(_, injectedVar) and
|
||||
command = ""
|
||||
or
|
||||
// the source flows to the injected variable with a command in between
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(command, injectedVar) and
|
||||
not command.regexpMatch(".*" + sanitizerCommand() + ".*")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it to declare env var.
|
||||
* e.g.
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* echo "FOO=$BODY" >> $GITHUB_ENV
|
||||
*/
|
||||
class EnvVarInjectionFromEnvVarSink extends EnvVarInjectionSink {
|
||||
string inVar;
|
||||
string injectedVar;
|
||||
string command;
|
||||
|
||||
EnvVarInjectionFromEnvVarSink() {
|
||||
exists(Run run |
|
||||
run.getScript() = this.asExpr() and
|
||||
exists(run.getInScopeEnvVarExpr(inVar)) and
|
||||
run.getScript().getAnEnvReachingGitHubEnvWrite(inVar, injectedVar) and
|
||||
(
|
||||
// the source flows to the injected variable without any command in between
|
||||
not run.getScript().getACmdReachingGitHubEnvWrite(_, injectedVar) and
|
||||
command = ""
|
||||
or
|
||||
// the source flows to the injected variable with a command in between
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(_, injectedVar) and
|
||||
run.getScript().getACmdReachingGitHubEnvWrite(command, injectedVar) and
|
||||
not command.regexpMatch(".*" + sanitizerCommand() + ".*")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a 3rd party action declares an environment variable with contents from an untrusted file.
|
||||
* e.g.
|
||||
*- name: Load .env file
|
||||
* uses: aarcangeli/load-dotenv@v1.0.0
|
||||
* with:
|
||||
* path: 'backend/new'
|
||||
* filenames: |
|
||||
* .env
|
||||
* .env.test
|
||||
* quiet: false
|
||||
* if-file-not-found: error
|
||||
*/
|
||||
class EnvVarInjectionFromMaDSink extends EnvVarInjectionSink {
|
||||
EnvVarInjectionFromMaDSink() { madSink(this, "envvar-injection") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate an environment variable.
|
||||
*/
|
||||
private module EnvVarInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RemoteFlowSource and
|
||||
not source.(RemoteFlowSource).getSourceType() = ["branch", "username"]
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof EnvVarInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScript() and
|
||||
(
|
||||
run.getScript().getAnEnvReachingGitHubEnvWrite(var, _)
|
||||
or
|
||||
run.getScript().getAnEnvReachingGitHubOutputWrite(var, _)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "envvar-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate an environment variable. */
|
||||
module EnvVarInjectionFlow = TaintTracking::Global<EnvVarInjectionConfig>;
|
||||
@@ -1,220 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
private import codeql.actions.security.ArtifactPoisoningQuery
|
||||
private import codeql.actions.security.UntrustedCheckoutQuery
|
||||
|
||||
abstract class OutputClobberingSink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares a step output variable with contents from a local file.
|
||||
* e.g.
|
||||
* run: |
|
||||
* cat test-results/.vars >> $GITHUB_OUTPUT
|
||||
* echo "sha=$(cat test-results/sha-number)" >> $GITHUB_OUTPUT
|
||||
* echo "sha=$(<test-results/sha-number)" >> $GITHUB_OUTPUT
|
||||
*/
|
||||
class OutputClobberingFromFileReadSink extends OutputClobberingSink {
|
||||
OutputClobberingFromFileReadSink() {
|
||||
exists(Run run, Step step, string field1, string field2 |
|
||||
(
|
||||
step instanceof UntrustedArtifactDownloadStep
|
||||
or
|
||||
step instanceof SimplePRHeadCheckoutStep
|
||||
) and
|
||||
step.getAFollowingStep() = run and
|
||||
this.asExpr() = run.getScript() and
|
||||
// A write to GITHUB_OUTPUT that is not attacker-controlled
|
||||
exists(string str |
|
||||
// The output of a command that is not a file read command
|
||||
run.getScript().getACmdReachingGitHubOutputWrite(str, field1) and
|
||||
not str = run.getScript().getAFileReadCommand()
|
||||
or
|
||||
// A hard-coded string
|
||||
run.getScript().getAWriteToGitHubOutput(field1, str) and
|
||||
str.regexpMatch("[\"'0-9a-zA-Z_\\-]+")
|
||||
) and
|
||||
// A write to GITHUB_OUTPUT that is attacker-controlled
|
||||
(
|
||||
// echo "sha=$(<test-results/sha-number)" >> $GITHUB_OUTPUT
|
||||
exists(string cmd |
|
||||
run.getScript().getACmdReachingGitHubOutputWrite(cmd, field2) and
|
||||
run.getScript().getAFileReadCommand() = cmd
|
||||
)
|
||||
or
|
||||
// cat test-results/.vars >> $GITHUB_OUTPUT
|
||||
run.getScript().fileToGitHubOutput(_) and
|
||||
field2 = "UNKNOWN"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it in a step variable output.
|
||||
* e.g.
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* echo "FOO=$BODY" >> $GITHUB_OUTPUT
|
||||
*/
|
||||
class OutputClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
OutputClobberingFromEnvVarSink() {
|
||||
exists(Run run, string field1, string field2 |
|
||||
// A write to GITHUB_OUTPUT that is attacker-controlled
|
||||
exists(string var |
|
||||
run.getScript().getAnEnvReachingGitHubOutputWrite(var, field1) and
|
||||
exists(run.getInScopeEnvVarExpr(var)) and
|
||||
run.getScript() = this.asExpr()
|
||||
) and
|
||||
// A write to GITHUB_OUTPUT that is not attacker-controlled
|
||||
exists(string str |
|
||||
// The output of a command that is not a file read command
|
||||
run.getScript().getACmdReachingGitHubOutputWrite(str, field2) and
|
||||
not str = run.getScript().getAFileReadCommand()
|
||||
or
|
||||
// A hard-coded string
|
||||
run.getScript().getAWriteToGitHubOutput(field2, str) and
|
||||
str.regexpMatch("[\"'0-9a-zA-Z_\\-]+")
|
||||
) and
|
||||
not field2 = field1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - id: clob1
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* echo $BODY
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* - id: clob2
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* echo $BODY
|
||||
*/
|
||||
class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
string clobbering_var;
|
||||
string clobbered_value;
|
||||
|
||||
WorkflowCommandClobberingFromEnvVarSink() {
|
||||
exists(Run run, string workflow_cmd_stmt, string clobbering_stmt |
|
||||
run.getScript() = this.asExpr() and
|
||||
run.getScript().getAStmt() = clobbering_stmt and
|
||||
clobbering_stmt.regexpMatch("echo\\s+(-e\\s+)?(\"|')?\\$(\\{)?" + clobbering_var + ".*") and
|
||||
exists(run.getInScopeEnvVarExpr(clobbering_var)) and
|
||||
run.getScript().getAStmt() = workflow_cmd_stmt and
|
||||
clobbered_value =
|
||||
trimQuotes(workflow_cmd_stmt.regexpCapture(".*::set-output\\s+name=.*::(.*)", 1))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - id: clob1
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* PR="$(<pr-number)"
|
||||
* echo "$PR"
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* - id: clob2
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* cat pr-number
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* - id: clob3
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* ls *.txt
|
||||
* - id: clob4
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
* echo "$CURRENT_VERSION"
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
*/
|
||||
class WorkflowCommandClobberingFromFileReadSink extends OutputClobberingSink {
|
||||
string clobbering_cmd;
|
||||
|
||||
WorkflowCommandClobberingFromFileReadSink() {
|
||||
exists(Run run, string clobbering_stmt |
|
||||
run.getScript() = this.asExpr() and
|
||||
run.getScript().getAStmt() = clobbering_stmt and
|
||||
(
|
||||
// A file's content is assigned to an env var that gets printed to stdout
|
||||
// - run: |
|
||||
// foo=$(<pr-id.txt)"
|
||||
// echo "${foo}"
|
||||
exists(string var, string value |
|
||||
run.getScript().getAnAssignment(var, value) and
|
||||
clobbering_cmd = run.getScript().getAFileReadCommand() and
|
||||
trimQuotes(value) = ["$(" + clobbering_cmd + ")", "`" + clobbering_cmd + "`"] and
|
||||
clobbering_stmt.regexpMatch("echo.*\\$(\\{)?" + var + ".*")
|
||||
)
|
||||
or
|
||||
// A file is read and its content is printed to stdout
|
||||
clobbering_cmd = run.getScript().getACommand() and
|
||||
clobbering_cmd.regexpMatch(["ls", Bash::fileReadCommand()] + "\\s.*") and
|
||||
(
|
||||
// - run: echo "foo=$(<pr-id.txt)"
|
||||
clobbering_stmt.regexpMatch("echo.*" + clobbering_cmd + ".*")
|
||||
or
|
||||
// A file content is printed to stdout
|
||||
// - run: cat pr-id.txt
|
||||
clobbering_stmt.indexOf(clobbering_cmd) = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OutputClobberingFromMaDSink extends OutputClobberingSink {
|
||||
OutputClobberingFromMaDSink() { madSink(this, "output-clobbering") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate an environment variable.
|
||||
*/
|
||||
private module OutputClobberingConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RemoteFlowSource and
|
||||
not source.(RemoteFlowSource).getSourceType() = "branch"
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof OutputClobberingSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScript() and
|
||||
run.getScript().getAWriteToGitHubOutput(_, _)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "output-clobbering")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
(
|
||||
exists(run.getScript().getAFileReadCommand()) or
|
||||
run.getScript().getAStmt().matches("%::set-output %")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate an environment variable. */
|
||||
module OutputClobberingFlow = TaintTracking::Global<OutputClobberingConfig>;
|
||||
@@ -1,56 +0,0 @@
|
||||
import actions
|
||||
|
||||
abstract class PoisonableStep extends Step { }
|
||||
|
||||
class DangerousActionUsesStep extends PoisonableStep, UsesStep {
|
||||
DangerousActionUsesStep() { poisonableActionsDataModel(this.getCallee()) }
|
||||
}
|
||||
|
||||
class PoisonableCommandStep extends PoisonableStep, Run {
|
||||
PoisonableCommandStep() {
|
||||
exists(string regexp |
|
||||
poisonableCommandsDataModel(regexp) and
|
||||
this.getScript().getACommand().regexpMatch(regexp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class JavascriptImportUsesStep extends PoisonableStep, UsesStep {
|
||||
JavascriptImportUsesStep() {
|
||||
exists(string script, string line |
|
||||
this.getCallee() = "actions/github-script" and
|
||||
script = this.getArgument("script") and
|
||||
line = script.splitAt("\n").trim() and
|
||||
// const { default: foo } = await import('${{ github.workspace }}/scripts/foo.mjs')
|
||||
// const script = require('${{ github.workspace }}/scripts/test.js');
|
||||
// const script = require('./scripts');
|
||||
line.regexpMatch(".*(import|require)\\(('|\")(\\./|.*github.workspace).*")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SetupNodeUsesStep extends PoisonableStep, UsesStep {
|
||||
SetupNodeUsesStep() {
|
||||
this.getCallee() = "actions/setup-node" and
|
||||
this.getArgument("cache") = "yarn"
|
||||
}
|
||||
}
|
||||
|
||||
class LocalScriptExecutionRunStep extends PoisonableStep, Run {
|
||||
string path;
|
||||
|
||||
LocalScriptExecutionRunStep() {
|
||||
exists(string cmd, string regexp, int path_group | cmd = this.getScript().getACommand() |
|
||||
poisonableLocalScriptsDataModel(regexp, path_group) and
|
||||
path = cmd.regexpCapture(regexp, path_group)
|
||||
)
|
||||
}
|
||||
|
||||
string getPath() { result = normalizePath(path.splitAt(" ")) }
|
||||
}
|
||||
|
||||
class LocalActionUsesStep extends PoisonableStep, UsesStep {
|
||||
LocalActionUsesStep() { this.getCallee().matches("./%") }
|
||||
|
||||
string getPath() { result = normalizePath(this.getCallee()) }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.DataFlow
|
||||
|
||||
private class RequestForgerySink extends DataFlow::Node {
|
||||
RequestForgerySink() { madSink(this, "request-forgery") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a system command.
|
||||
*/
|
||||
private module RequestForgeryConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgerySink }
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a system command. */
|
||||
module RequestForgeryFlow = TaintTracking::Global<RequestForgeryConfig>;
|
||||
@@ -1,21 +0,0 @@
|
||||
private import actions
|
||||
private import codeql.actions.TaintTracking
|
||||
private import codeql.actions.dataflow.ExternalFlow
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import codeql.actions.DataFlow
|
||||
|
||||
private class SecretExfiltrationSink extends DataFlow::Node {
|
||||
SecretExfiltrationSink() { madSink(this, "secret-exfiltration") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for untrusted data that reaches a sink where it may lead to secret exfiltration
|
||||
*/
|
||||
private module SecretExfiltrationConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof SecretExfiltrationSink }
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used in a context where it may lead to a secret exfiltration. */
|
||||
module SecretExfiltrationFlow = TaintTracking::Global<SecretExfiltrationConfig>;
|
||||
@@ -1,45 +0,0 @@
|
||||
import actions
|
||||
|
||||
bindingset[runner]
|
||||
predicate isGithubHostedRunner(string runner) {
|
||||
// list of github hosted repos: https://github.com/actions/runner-images/blob/main/README.md#available-images
|
||||
runner
|
||||
.toLowerCase()
|
||||
.regexpMatch("^(ubuntu-([0-9.]+|latest)|macos-([0-9]+|latest)(-x?large)?|windows-([0-9.]+|latest))$")
|
||||
}
|
||||
|
||||
bindingset[runner]
|
||||
predicate is3rdPartyHostedRunner(string runner) {
|
||||
runner.toLowerCase().regexpMatch("^(buildjet|warp)-[a-z0-9-]+$")
|
||||
}
|
||||
|
||||
/**
|
||||
* This predicate uses data available in the workflow file to identify self-hosted runners.
|
||||
* It does not know if the repository is public or private.
|
||||
* It is a best-effort approach to identify self-hosted runners.
|
||||
*/
|
||||
predicate staticallyIdentifiedSelfHostedRunner(Job job) {
|
||||
exists(string label |
|
||||
job.getATriggerEvent().getName() =
|
||||
[
|
||||
"issue_comment", "pull_request", "pull_request_review", "pull_request_review_comment",
|
||||
"pull_request_target", "workflow_run"
|
||||
] and
|
||||
label = job.getARunsOnLabel() and
|
||||
not isGithubHostedRunner(label) and
|
||||
not is3rdPartyHostedRunner(label)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This predicate uses data available in the job log files to identify self-hosted runners.
|
||||
* It is a best-effort approach to identify self-hosted runners.
|
||||
*/
|
||||
predicate dynamicallyIdentifiedSelfHostedRunner(Job job) {
|
||||
exists(string runner_info |
|
||||
repositoryDataModel("public", _) and
|
||||
workflowDataModel(job.getEnclosingWorkflow().getLocation().getFile().getRelativePath(), _,
|
||||
job.getId(), _, _, runner_info) and
|
||||
runner_info.indexOf("self-hosted:true") > 0
|
||||
)
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
import actions
|
||||
private import codeql.actions.DataFlow
|
||||
private import codeql.actions.dataflow.FlowSources
|
||||
private import codeql.actions.TaintTracking
|
||||
|
||||
string checkoutTriggers() {
|
||||
result = ["pull_request_target", "workflow_run", "workflow_call", "issue_comment"]
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for PR HEAD references flowing
|
||||
* into actions/checkout's ref argument.
|
||||
*/
|
||||
private module ActionsMutableRefCheckoutConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
(
|
||||
// remote flow sources
|
||||
source instanceof GitHubCtxSource
|
||||
or
|
||||
source instanceof GitHubEventCtxSource
|
||||
or
|
||||
source instanceof GitHubEventJsonSource
|
||||
or
|
||||
source instanceof MaDSource
|
||||
or
|
||||
// `ref` argument contains the PR id/number or head ref
|
||||
exists(Expression e |
|
||||
source.asExpr() = e and
|
||||
(
|
||||
containsHeadRef(e.getExpression()) or
|
||||
containsPullRequestNumber(e.getExpression())
|
||||
)
|
||||
)
|
||||
or
|
||||
// 3rd party actions returning the PR head ref
|
||||
exists(StepsExpression e, UsesStep step |
|
||||
source.asExpr() = e and
|
||||
e.getStepId() = step.getId() and
|
||||
(
|
||||
step.getCallee() = "eficode/resolve-pr-refs" and e.getFieldName() = "head_ref"
|
||||
or
|
||||
step.getCallee() = "xt0rted/pull-request-comment-branch" and e.getFieldName() = "head_ref"
|
||||
or
|
||||
step.getCallee() = "alessbell/pull-request-comment-branch" and
|
||||
e.getFieldName() = "head_ref"
|
||||
or
|
||||
step.getCallee() = "gotson/pull-request-comment-branch" and e.getFieldName() = "head_ref"
|
||||
or
|
||||
step.getCallee() = "potiuk/get-workflow-origin" and
|
||||
e.getFieldName() = ["sourceHeadBranch", "pullRequestNumber"]
|
||||
or
|
||||
step.getCallee() = "github/branch-deploy" and e.getFieldName() = ["ref", "fork_ref"]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) {
|
||||
exists(Uses uses |
|
||||
uses.getCallee() = "actions/checkout" and
|
||||
uses.getArgumentExpr(["ref", "repository"]) = sink.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module ActionsMutableRefCheckoutFlow = TaintTracking::Global<ActionsMutableRefCheckoutConfig>;
|
||||
|
||||
private module ActionsSHACheckoutConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
source.asExpr().getATriggerEvent().getName() =
|
||||
["pull_request_target", "workflow_run", "workflow_call", "issue_comment"] and
|
||||
(
|
||||
// `ref` argument contains the PR head/merge commit sha
|
||||
exists(Expression e |
|
||||
source.asExpr() = e and
|
||||
containsHeadSHA(e.getExpression())
|
||||
)
|
||||
or
|
||||
// 3rd party actions returning the PR head sha
|
||||
exists(StepsExpression e, UsesStep step |
|
||||
source.asExpr() = e and
|
||||
e.getStepId() = step.getId() and
|
||||
(
|
||||
step.getCallee() = "eficode/resolve-pr-refs" and e.getFieldName() = "head_sha"
|
||||
or
|
||||
step.getCallee() = "xt0rted/pull-request-comment-branch" and e.getFieldName() = "head_sha"
|
||||
or
|
||||
step.getCallee() = "alessbell/pull-request-comment-branch" and
|
||||
e.getFieldName() = "head_sha"
|
||||
or
|
||||
step.getCallee() = "gotson/pull-request-comment-branch" and e.getFieldName() = "head_sha"
|
||||
or
|
||||
step.getCallee() = "potiuk/get-workflow-origin" and
|
||||
e.getFieldName() = ["sourceHeadSha", "mergeCommitSha"]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) {
|
||||
exists(Uses uses |
|
||||
uses.getCallee() = "actions/checkout" and
|
||||
uses.getArgumentExpr(["ref", "repository"]) = sink.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScript() and
|
||||
exists(run.getScript().getAFileReadCommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module ActionsSHACheckoutFlow = TaintTracking::Global<ActionsSHACheckoutConfig>;
|
||||
|
||||
bindingset[s]
|
||||
predicate containsPullRequestNumber(string s) {
|
||||
exists(
|
||||
normalizeExpr(s)
|
||||
.regexpFind([
|
||||
"\\bgithub\\.event\\.number\\b", "\\bgithub\\.event\\.issue\\.number\\b",
|
||||
"\\bgithub\\.event\\.pull_request\\.id\\b",
|
||||
"\\bgithub\\.event\\.pull_request\\.number\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.pull_requests\\[\\d+\\]\\.id\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.pull_requests\\[\\d+\\]\\.number\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.pull_requests\\[\\d+\\]\\.id\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.pull_requests\\[\\d+\\]\\.number\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.pull_requests\\[\\d+\\]\\.id\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.pull_requests\\[\\d+\\]\\.number\\b",
|
||||
// heuristics
|
||||
"\\bpr_number\\b", "\\bpr_id\\b"
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[s]
|
||||
predicate containsHeadSHA(string s) {
|
||||
exists(
|
||||
normalizeExpr(s)
|
||||
.regexpFind([
|
||||
"\\bgithub\\.event\\.pull_request\\.head\\.sha\\b",
|
||||
"\\bgithub\\.event\\.pull_request\\.merge_commit_sha\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_commit\\.id\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.after\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.head_commit\\.id\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.pull_requests\\[\\d+\\]\\.head\\.sha\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.after\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.head_commit\\.id\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.pull_requests\\[\\d+\\]\\.head\\.sha\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.pull_requests\\[\\d+\\]\\.head\\.sha\\b",
|
||||
"\\bgithub\\.event\\.merge_group\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.merge_group\\.head_commit\\.id\\b",
|
||||
// heuristics
|
||||
"\\bhead\\.sha\\b", "\\bhead_sha\\b", "\\bmerge_sha\\b", "\\bpr_head_sha\\b"
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[s]
|
||||
predicate containsHeadRef(string s) {
|
||||
exists(
|
||||
normalizeExpr(s)
|
||||
.regexpFind([
|
||||
"\\bgithub\\.event\\.pull_request\\.head\\.ref\\b", "\\bgithub\\.head_ref\\b",
|
||||
"\\bgithub\\.event\\.workflow_run\\.head_branch\\b",
|
||||
"\\bgithub\\.event\\.check_suite\\.pull_requests\\[\\d+\\]\\.head\\.ref\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.check_suite\\.pull_requests\\[\\d+\\]\\.head\\.ref\\b",
|
||||
"\\bgithub\\.event\\.check_run\\.pull_requests\\[\\d+\\]\\.head\\.ref\\b",
|
||||
"\\bgithub\\.event\\.merge_group\\.head_ref\\b",
|
||||
// heuristics
|
||||
"\\bhead\\.ref\\b", "\\bhead_ref\\b", "\\bmerge_ref\\b", "\\bpr_head_ref\\b",
|
||||
// env vars
|
||||
"GITHUB_HEAD_REF",
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
|
||||
class SimplePRHeadCheckoutStep extends Step {
|
||||
SimplePRHeadCheckoutStep() {
|
||||
// This should be:
|
||||
// artifact instanceof PRHeadCheckoutStep
|
||||
// but PRHeadCheckoutStep uses Taint Tracking anc causes a non-Monolitic Recursion error
|
||||
// so we list all the subclasses of PRHeadCheckoutStep here and use actions/checkout as a workaround
|
||||
// instead of using ActionsMutableRefCheckout and ActionsSHACheckout
|
||||
exists(Uses uses |
|
||||
this = uses and
|
||||
uses.getCallee() = "actions/checkout" and
|
||||
exists(uses.getArgument("ref")) and
|
||||
not uses.getArgument("ref").matches("%base%") and
|
||||
uses.getATriggerEvent().getName() = checkoutTriggers()
|
||||
)
|
||||
or
|
||||
this instanceof GitMutableRefCheckout
|
||||
or
|
||||
this instanceof GitSHACheckout
|
||||
or
|
||||
this instanceof GhMutableRefCheckout
|
||||
or
|
||||
this instanceof GhSHACheckout
|
||||
}
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD */
|
||||
abstract class PRHeadCheckoutStep extends Step {
|
||||
abstract string getPath();
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref */
|
||||
abstract class MutableRefCheckoutStep extends PRHeadCheckoutStep { }
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref */
|
||||
abstract class SHACheckoutStep extends PRHeadCheckoutStep { }
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using actions/checkout action */
|
||||
class ActionsMutableRefCheckout extends MutableRefCheckoutStep instanceof UsesStep {
|
||||
ActionsMutableRefCheckout() {
|
||||
this.getCallee() = "actions/checkout" and
|
||||
(
|
||||
exists(
|
||||
ActionsMutableRefCheckoutFlow::PathNode source, ActionsMutableRefCheckoutFlow::PathNode sink
|
||||
|
|
||||
ActionsMutableRefCheckoutFlow::flowPath(source, sink) and
|
||||
this.getArgumentExpr(["ref", "repository"]) = sink.getNode().asExpr()
|
||||
)
|
||||
or
|
||||
// heuristic base on the step id and field name
|
||||
exists(string value, Expression expr |
|
||||
value.regexpMatch(".*(head|branch|ref).*") and expr = this.getArgumentExpr("ref")
|
||||
|
|
||||
expr.(StepsExpression).getStepId() = value
|
||||
or
|
||||
expr.(SimpleReferenceExpression).getFieldName() = value and
|
||||
not expr instanceof GitHubExpression
|
||||
or
|
||||
expr.(NeedsExpression).getNeededJobId() = value
|
||||
or
|
||||
expr.(JsonReferenceExpression).getAccessPath() = value
|
||||
or
|
||||
expr.(JsonReferenceExpression).getInnerExpression() = value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if exists(this.(UsesStep).getArgument("path"))
|
||||
then result = this.(UsesStep).getArgument("path")
|
||||
else result = "GITHUB_WORKSPACE/"
|
||||
}
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using actions/checkout action */
|
||||
class ActionsSHACheckout extends SHACheckoutStep instanceof UsesStep {
|
||||
ActionsSHACheckout() {
|
||||
this.getCallee() = "actions/checkout" and
|
||||
(
|
||||
exists(ActionsSHACheckoutFlow::PathNode source, ActionsSHACheckoutFlow::PathNode sink |
|
||||
ActionsSHACheckoutFlow::flowPath(source, sink) and
|
||||
this.getArgumentExpr(["ref", "repository"]) = sink.getNode().asExpr()
|
||||
)
|
||||
or
|
||||
// heuristic base on the step id and field name
|
||||
exists(string value, Expression expr |
|
||||
value.regexpMatch(".*(head|sha|commit).*") and expr = this.getArgumentExpr("ref")
|
||||
|
|
||||
expr.(StepsExpression).getStepId() = value
|
||||
or
|
||||
expr.(SimpleReferenceExpression).getFieldName() = value and
|
||||
not expr instanceof GitHubExpression
|
||||
or
|
||||
expr.(NeedsExpression).getNeededJobId() = value
|
||||
or
|
||||
expr.(JsonReferenceExpression).getAccessPath() = value
|
||||
or
|
||||
expr.(JsonReferenceExpression).getInnerExpression() = value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() {
|
||||
if exists(this.(UsesStep).getArgument("path"))
|
||||
then result = this.(UsesStep).getArgument("path")
|
||||
else result = "GITHUB_WORKSPACE/"
|
||||
}
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using git within a Run step */
|
||||
class GitMutableRefCheckout extends MutableRefCheckoutStep instanceof Run {
|
||||
GitMutableRefCheckout() {
|
||||
exists(string cmd | this.getScript().getACommand() = cmd |
|
||||
cmd.regexpMatch("git\\s+(fetch|pull).*") and
|
||||
(
|
||||
(containsHeadRef(cmd) or containsPullRequestNumber(cmd))
|
||||
or
|
||||
exists(string varname, string expr |
|
||||
expr = this.getInScopeEnvVarExpr(varname).getExpression() and
|
||||
(
|
||||
containsHeadRef(expr) or
|
||||
containsPullRequestNumber(expr)
|
||||
) and
|
||||
exists(cmd.regexpFind(varname, _, _))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() { result = this.(Run).getWorkingDirectory() }
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using git within a Run step */
|
||||
class GitSHACheckout extends SHACheckoutStep instanceof Run {
|
||||
GitSHACheckout() {
|
||||
exists(string cmd | this.getScript().getACommand() = cmd |
|
||||
cmd.regexpMatch("git\\s+(fetch|pull).*") and
|
||||
(
|
||||
containsHeadSHA(cmd)
|
||||
or
|
||||
exists(string varname, string expr |
|
||||
expr = this.getInScopeEnvVarExpr(varname).getExpression() and
|
||||
containsHeadSHA(expr) and
|
||||
exists(cmd.regexpFind(varname, _, _))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() { result = this.(Run).getWorkingDirectory() }
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using gh within a Run step */
|
||||
class GhMutableRefCheckout extends MutableRefCheckoutStep instanceof Run {
|
||||
GhMutableRefCheckout() {
|
||||
exists(string cmd | this.getScript().getACommand() = cmd |
|
||||
cmd.regexpMatch(".*(gh|hub)\\s+pr\\s+checkout.*") and
|
||||
(
|
||||
(containsHeadRef(cmd) or containsPullRequestNumber(cmd))
|
||||
or
|
||||
exists(string varname |
|
||||
(
|
||||
containsHeadRef(this.getInScopeEnvVarExpr(varname).getExpression()) or
|
||||
containsPullRequestNumber(this.getInScopeEnvVarExpr(varname).getExpression())
|
||||
) and
|
||||
exists(cmd.regexpFind(varname, _, _))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() { result = this.(Run).getWorkingDirectory() }
|
||||
}
|
||||
|
||||
/** Checkout of a Pull Request HEAD ref using gh within a Run step */
|
||||
class GhSHACheckout extends SHACheckoutStep instanceof Run {
|
||||
GhSHACheckout() {
|
||||
exists(string cmd | this.getScript().getACommand() = cmd |
|
||||
cmd.regexpMatch("gh\\s+pr\\s+checkout.*") and
|
||||
(
|
||||
containsHeadSHA(cmd)
|
||||
or
|
||||
exists(string varname |
|
||||
containsHeadSHA(this.getInScopeEnvVarExpr(varname).getExpression()) and
|
||||
exists(cmd.regexpFind(varname, _, _))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getPath() { result = this.(Run).getWorkingDirectory() }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import actions
|
||||
|
||||
class KnownVulnerableAction extends UsesStep {
|
||||
string vulnerable_action;
|
||||
string fixed_version;
|
||||
string vulnerable_version;
|
||||
string vulnerable_sha;
|
||||
|
||||
KnownVulnerableAction() {
|
||||
vulnerableActionsDataModel(vulnerable_action, vulnerable_version, vulnerable_sha, fixed_version) and
|
||||
this.getCallee() = vulnerable_action and
|
||||
(this.getVersion() = vulnerable_version or this.getVersion() = vulnerable_sha)
|
||||
}
|
||||
|
||||
string getFixedVersion() { result = fixed_version }
|
||||
|
||||
string getVulnerableAction() { result = vulnerable_action }
|
||||
|
||||
string getVulnerableVersion() { result = vulnerable_version }
|
||||
|
||||
string getVulnerableSha() { result = vulnerable_sha }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import actions
|
||||
|
||||
class UnversionedImmutableAction extends UsesStep {
|
||||
string immutable_action;
|
||||
|
||||
UnversionedImmutableAction() {
|
||||
isImmutableAction(this, immutable_action) and
|
||||
not isSemVer(this.getVersion())
|
||||
}
|
||||
}
|
||||
|
||||
bindingset[version]
|
||||
predicate isSemVer(string version) {
|
||||
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string with optional v prefix
|
||||
version
|
||||
.regexpMatch("^v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$") or
|
||||
// or N or N.x or N.N.x with optional v prefix
|
||||
version.regexpMatch("^v?[1-9]\\d*$") or
|
||||
version.regexpMatch("^v?[1-9]\\d*\\.(x|0|([1-9]\\d*))$") or
|
||||
version.regexpMatch("^v?[1-9]\\d*\\.(0|([1-9]\\d*))\\.(x|0|([1-9]\\d*))$") or
|
||||
// or latest which will work
|
||||
version = "latest"
|
||||
}
|
||||
|
||||
predicate isImmutableAction(UsesStep actionStep, string actionName) {
|
||||
immutableActionsDataModel(actionName) and
|
||||
actionStep.getCallee() = actionName
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
/** Provides classes for working with files and folders. */
|
||||
|
||||
private import codeql.Locations
|
||||
|
||||
/** A file or folder. */
|
||||
abstract class Container extends @container {
|
||||
/** Gets a file or sub-folder in this container. */
|
||||
Container getAChildContainer() { this = result.getParentContainer() }
|
||||
|
||||
/** Gets a file in this container. */
|
||||
File getAFile() { result = this.getAChildContainer() }
|
||||
|
||||
/** Gets a sub-folder in this container. */
|
||||
Folder getAFolder() { result = this.getAChildContainer() }
|
||||
|
||||
/**
|
||||
* Gets the absolute, canonical path of this container, using forward slashes
|
||||
* as path separator.
|
||||
*
|
||||
* The path starts with a _root prefix_ followed by zero or more _path
|
||||
* segments_ separated by forward slashes.
|
||||
*
|
||||
* The root prefix is of one of the following forms:
|
||||
*
|
||||
* 1. A single forward slash `/` (Unix-style)
|
||||
* 2. An upper-case drive letter followed by a colon and a forward slash,
|
||||
* such as `C:/` (Windows-style)
|
||||
* 3. Two forward slashes, a computer name, and then another forward slash,
|
||||
* such as `//FileServer/` (UNC-style)
|
||||
*
|
||||
* Path segments are never empty (that is, absolute paths never contain two
|
||||
* contiguous slashes, except as part of a UNC-style root prefix). Also, path
|
||||
* segments never contain forward slashes, and no path segment is of the
|
||||
* form `.` (one dot) or `..` (two dots).
|
||||
*
|
||||
* Note that an absolute path never ends with a forward slash, except if it is
|
||||
* a bare root prefix, that is, the path has no path segments. A container
|
||||
* whose absolute path has no segments is always a `Folder`, not a `File`.
|
||||
*/
|
||||
abstract string getAbsolutePath();
|
||||
|
||||
/**
|
||||
* Gets the base name of this container including extension, that is, the last
|
||||
* segment of its absolute path, or the empty string if it has no segments.
|
||||
*
|
||||
* Here are some examples of absolute paths and the corresponding base names
|
||||
* (surrounded with quotes to avoid ambiguity):
|
||||
*
|
||||
* <table border="1">
|
||||
* <tr><th>Absolute path</th><th>Base name</th></tr>
|
||||
* <tr><td>"/tmp/tst.go"</td><td>"tst.go"</td></tr>
|
||||
* <tr><td>"C:/Program Files (x86)"</td><td>"Program Files (x86)"</td></tr>
|
||||
* <tr><td>"/"</td><td>""</td></tr>
|
||||
* <tr><td>"C:/"</td><td>""</td></tr>
|
||||
* <tr><td>"D:/"</td><td>""</td></tr>
|
||||
* <tr><td>"//FileServer/"</td><td>""</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
string getBaseName() {
|
||||
result = this.getAbsolutePath().regexpCapture(".*/(([^/]*?)(?:\\.([^.]*))?)", 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension of this container, that is, the suffix of its base name
|
||||
* after the last dot character, if any.
|
||||
*
|
||||
* In particular,
|
||||
*
|
||||
* - if the name does not include a dot, there is no extension, so this
|
||||
* predicate has no result;
|
||||
* - if the name ends in a dot, the extension is the empty string;
|
||||
* - if the name contains multiple dots, the extension follows the last dot.
|
||||
*
|
||||
* Here are some examples of absolute paths and the corresponding extensions
|
||||
* (surrounded with quotes to avoid ambiguity):
|
||||
*
|
||||
* <table border="1">
|
||||
* <tr><th>Absolute path</th><th>Extension</th></tr>
|
||||
* <tr><td>"/tmp/tst.go"</td><td>"go"</td></tr>
|
||||
* <tr><td>"/tmp/.classpath"</td><td>"classpath"</td></tr>
|
||||
* <tr><td>"/bin/bash"</td><td>not defined</td></tr>
|
||||
* <tr><td>"/tmp/tst2."</td><td>""</td></tr>
|
||||
* <tr><td>"/tmp/x.tar.gz"</td><td>"gz"</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
string getExtension() {
|
||||
result = this.getAbsolutePath().regexpCapture(".*/([^/]*?)(\\.([^.]*))?", 3)
|
||||
}
|
||||
|
||||
/** Gets the file in this container that has the given `baseName`, if any. */
|
||||
File getFile(string baseName) {
|
||||
result = this.getAFile() and
|
||||
result.getBaseName() = baseName
|
||||
}
|
||||
|
||||
/** Gets the sub-folder in this container that has the given `baseName`, if any. */
|
||||
Folder getFolder(string baseName) {
|
||||
result = this.getAFolder() and
|
||||
result.getBaseName() = baseName
|
||||
}
|
||||
|
||||
/** Gets the parent container of this file or folder, if any. */
|
||||
Container getParentContainer() { containerparent(result, this) }
|
||||
|
||||
/**
|
||||
* Gets the relative path of this file or folder from the root folder of the
|
||||
* analyzed source location. The relative path of the root folder itself is
|
||||
* the empty string.
|
||||
*
|
||||
* This has no result if the container is outside the source root, that is,
|
||||
* if the root folder is not a reflexive, transitive parent of this container.
|
||||
*/
|
||||
string getRelativePath() {
|
||||
exists(string absPath, string pref |
|
||||
absPath = this.getAbsolutePath() and sourceLocationPrefix(pref)
|
||||
|
|
||||
absPath = pref and result = ""
|
||||
or
|
||||
absPath = pref.regexpReplaceAll("/$", "") + "/" + result and
|
||||
not result.matches("/%")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stem of this container, that is, the prefix of its base name up to
|
||||
* (but not including) the last dot character if there is one, or the entire
|
||||
* base name if there is not.
|
||||
*
|
||||
* Here are some examples of absolute paths and the corresponding stems
|
||||
* (surrounded with quotes to avoid ambiguity):
|
||||
*
|
||||
* <table border="1">
|
||||
* <tr><th>Absolute path</th><th>Stem</th></tr>
|
||||
* <tr><td>"/tmp/tst.go"</td><td>"tst"</td></tr>
|
||||
* <tr><td>"/tmp/.classpath"</td><td>""</td></tr>
|
||||
* <tr><td>"/bin/bash"</td><td>"bash"</td></tr>
|
||||
* <tr><td>"/tmp/tst2."</td><td>"tst2"</td></tr>
|
||||
* <tr><td>"/tmp/x.tar.gz"</td><td>"x.tar"</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
string getStem() {
|
||||
result = this.getAbsolutePath().regexpCapture(".*/([^/]*?)(?:\\.([^.]*))?", 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a URL representing the location of this container.
|
||||
*
|
||||
* For more information see https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/#providing-urls.
|
||||
*/
|
||||
abstract string getURL();
|
||||
|
||||
/**
|
||||
* Gets a textual representation of the path of this container.
|
||||
*
|
||||
* This is the absolute path of the container.
|
||||
*/
|
||||
string toString() { result = this.getAbsolutePath() }
|
||||
}
|
||||
|
||||
/** A folder. */
|
||||
class Folder extends Container, @folder {
|
||||
override string getAbsolutePath() { folders(this, result) }
|
||||
|
||||
/** Gets the URL of this folder. */
|
||||
override string getURL() { result = "folder://" + this.getAbsolutePath() }
|
||||
}
|
||||
|
||||
/** A file. */
|
||||
class File extends Container, @file {
|
||||
override string getAbsolutePath() { files(this, result) }
|
||||
|
||||
/** Gets the URL of this file. */
|
||||
override string getURL() { result = "file://" + this.getAbsolutePath() + ":0:0:0:0" }
|
||||
|
||||
/** Holds if this file was extracted from ordinary source code. */
|
||||
predicate fromSource() { any() }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: argumentInjectionSinksDataModel
|
||||
# https://gtfobins.github.io/
|
||||
# https://0xn3va.gitbook.io/cheat-sheets/web-application/command-injection/argument-injection
|
||||
data:
|
||||
- ["(awk)\\s(.*?)", 1, 2]
|
||||
- ["(find)\\s(.*?)", 1, 2]
|
||||
- ["(git clone)\\s(.*?)", 1, 2]
|
||||
- ["(sed)\\s(.*?)", 1, 2]
|
||||
- ["(tar)\\s(.*?)", 1, 2]
|
||||
- ["(wget)\\s(.*?)", 1, 2]
|
||||
- ["(zip)\\s(.*?)", 1, 2]
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: contextTriggerDataModel
|
||||
data:
|
||||
- ["commit_comment", "github.event.comment"]
|
||||
- ["commit_comment", "github.event.changes"]
|
||||
- ["discussion", "github.event.discussion"]
|
||||
- ["discussion", "github.event.changes"]
|
||||
- ["discussion_comment", "github.event.comment"]
|
||||
- ["discussion_comment", "github.event.discussion"]
|
||||
- ["discussion_comment", "github.event.changes"]
|
||||
- ["issues", "github.event.issue"]
|
||||
- ["issues", "github.event.changes"]
|
||||
- ["issue_comment", "github.event.issue"]
|
||||
- ["issue_comment", "github.event.comment"]
|
||||
- ["issue_comment", "github.event.changes"]
|
||||
- ["gollum", "github.event.pages"]
|
||||
- ["gollum", "github.event.changes"]
|
||||
- ["pull_request_comment", "github.event.comment"]
|
||||
- ["pull_request_comment", "github.event.pull_request"]
|
||||
- ["pull_request_comment", "github.head_ref"]
|
||||
- ["pull_request_comment", "github.event.changes"]
|
||||
- ["pull_request_review", "github.event.pull_request"]
|
||||
- ["pull_request_review", "github.event.review"]
|
||||
- ["pull_request_review", "github.head_ref"]
|
||||
- ["pull_request_review", "github.event.changes"]
|
||||
- ["pull_request_review_comment", "github.event.comment"]
|
||||
- ["pull_request_review_comment", "github.event.pull_request"]
|
||||
- ["pull_request_review_comment", "github.event.review"]
|
||||
- ["pull_request_review_comment", "github.head_ref"]
|
||||
- ["pull_request_review_comment", "github.event.changes"]
|
||||
- ["pull_request_target", "github.event.pull_request"]
|
||||
- ["pull_request_target", "github.head_ref"]
|
||||
- ["pull_request_target", "github.event.changes"]
|
||||
- ["push", "github.event.commits"]
|
||||
- ["push", "github.event.head_commit"]
|
||||
- ["push", "github.event.changes"]
|
||||
- ["workflow_run", "github.event.workflow"]
|
||||
- ["workflow_run", "github.event.workflow_run"]
|
||||
- ["workflow_run", "github.event.changes"]
|
||||
# workflow_call receives the same event payload as the calling workflow
|
||||
- ["workflow_call", "github.event.comment"]
|
||||
- ["workflow_call", "github.event.discussion"]
|
||||
- ["workflow_call", "github.event.inputs"]
|
||||
- ["workflow_call", "github.event.issue"]
|
||||
- ["workflow_call", "github.event.pages"]
|
||||
- ["workflow_call", "github.event.pull_request"]
|
||||
- ["workflow_call", "github.event.review"]
|
||||
- ["workflow_call", "github.event.workflow"]
|
||||
- ["workflow_call", "github.event.workflow_run"]
|
||||
- ["workflow_call", "github.event.changes"]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: externallyTriggerableEventsDataModel
|
||||
data:
|
||||
- ["discussion"]
|
||||
- ["discussion_comment"]
|
||||
- ["fork"]
|
||||
- ["watch"]
|
||||
- ["issue_comment"]
|
||||
- ["issues"]
|
||||
- ["pull_request_comment"]
|
||||
- ["pull_request_review"]
|
||||
- ["pull_request_review_comment"]
|
||||
- ["pull_request_target"]
|
||||
- ["workflow_run"] # depending on branch filter
|
||||
- ["workflow_call"] # depending on caller
|
||||
- ["workflow_dispatch"]
|
||||
- ["scheduled"]
|
||||
@@ -1,10 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: immutableActionsDataModel
|
||||
# Since the Immutable Actions feature is not yet available to customers, we won't alert about
|
||||
# any unversioned immutable action references for now. Within GitHub, we'll include the
|
||||
# `codeql/immutable-actions-list` model pack, which will provide the necessary list of actions
|
||||
# for internal use. Once the feature is available to customers, we'll move that list back into
|
||||
# this file.
|
||||
data: []
|
||||
@@ -1,76 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: poisonableActionsDataModel
|
||||
# source: https://boostsecurityio.github.io/lotp/
|
||||
data:
|
||||
- ["azure/powershell"]
|
||||
- ["pre-commit/action"]
|
||||
- ["oxsecurity/megalinter"]
|
||||
- ["bridgecrewio/checkov-action"]
|
||||
- ["ruby/setup-ruby"]
|
||||
- ["actions/jekyll-build-pages"]
|
||||
- ["qcastel/github-actions-maven/actions/maven"]
|
||||
- ["sonarsource/sonarcloud-github-action"]
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: poisonableCommandsDataModel
|
||||
# source: https://boostsecurityio.github.io/lotp/
|
||||
data:
|
||||
- ["ant"]
|
||||
- ["asv"]
|
||||
- ["awk\\s+-f"]
|
||||
- ["bundle"]
|
||||
- ["bun"]
|
||||
- ["cargo"]
|
||||
- ["checkov"]
|
||||
- ["eslint"]
|
||||
- ["gcloud\\s+builds submit"]
|
||||
- ["golangci-lint"]
|
||||
- ["gomplate"]
|
||||
- ["goreleaser"]
|
||||
- ["gradle"]
|
||||
- ["java\\s+-jar"]
|
||||
- ["make"]
|
||||
- ["mdformat"]
|
||||
- ["mkdocs"]
|
||||
- ["msbuild"]
|
||||
- ["mvn"]
|
||||
- ["mypy"]
|
||||
- ["(p)?npm\\s+[a-z]"]
|
||||
- ["pre-commit"]
|
||||
- ["prettier"]
|
||||
- ["phpstan"]
|
||||
- ["pip\\s+install(.*)\\s+-r"]
|
||||
- ["pip\\s+install(.*)\\s+--requirement"]
|
||||
- ["pip(x)?\\s+install(.*)\\s+\\."]
|
||||
- ["poetry"]
|
||||
- ["pylint"]
|
||||
- ["pytest"]
|
||||
- ["python[\\d\\.]*\\s+-m\\s+pip\\s+install\\s+-r"]
|
||||
- ["python[\\d\\.]*\\s+-m\\s+pip\\s+install\\s+--requirement"]
|
||||
- ["rake"]
|
||||
- ["rails\\s+db:create"]
|
||||
- ["rails\\s+assets:precompile"]
|
||||
- ["rubocop"]
|
||||
- ["sed\\s+-f"]
|
||||
- ["sonar-scanner"]
|
||||
- ["stylelint"]
|
||||
- ["terraform"]
|
||||
- ["tflint"]
|
||||
- ["yarn"]
|
||||
- ["webpack"]
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: poisonableLocalScriptsDataModel
|
||||
data:
|
||||
# TODO: It could also be in the form of `dir/cmd`
|
||||
- ["(\\.\\/[^\\s]+)\\b", 1] # eg: ./venv/bin/activate
|
||||
- ["(\\.\\s+[^\\s]+)\\b", 1] # eg: . venv/bin/activate
|
||||
- ["(source|sh|bash|zsh|fish)\\s+([^\\s]+)\\b", 2]
|
||||
- ["(node)\\s+([^\\s]+)(\\.js|\\.ts)\\b", 2]
|
||||
- ["(python[\\d\\.]*)\\s+([^\\s]+)\\.py\\b", 2]
|
||||
- ["(ruby)\\s+([^\\s]+)\\.rb\\b", 2]
|
||||
- ["(go)\\s+(generate|run)\\s+([^\\s]+)\\.go\\b", 3]
|
||||
- ["(dotnet)\\s+([^\\s]+)\\.csproj\\b", 2]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: trustedActionsOwnerDataModel
|
||||
data:
|
||||
- ["actions"]
|
||||
- ["github"]
|
||||
- ["advanced-security"]
|
||||
@@ -1,84 +0,0 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/actions-all
|
||||
extensible: untrustedEventPropertiesDataModel
|
||||
data:
|
||||
# TITLE
|
||||
- ["github\\.event\\.issue\\.title", "title"]
|
||||
- ["github\\.event\\.pull_request\\.title", "title"]
|
||||
- ["github\\.event\\.discussion\\.title", "title"]
|
||||
- ["github\\.event\\.pages\\[[0-9]+\\]\\.page_name", "title"]
|
||||
- ["github\\.event\\.pages\\[[0-9]+\\]\\.title", "title"]
|
||||
- ["github\\.event\\.workflow_run\\.display_title", "title"]
|
||||
- ["github\\.event\\.changes\\.title\\.from", "title"]
|
||||
# URL
|
||||
- ["github\\.event\\.pull_request\\.head\\.repo\\.homepage", "url"]
|
||||
# TEXT
|
||||
- ["github\\.event\\.issue\\.body", "text"]
|
||||
- ["github\\.event\\.pull_request\\.body", "text"]
|
||||
- ["github\\.event\\.discussion\\.body", "text"]
|
||||
- ["github\\.event\\.review\\.body", "text"]
|
||||
- ["github\\.event\\.comment\\.body", "text"]
|
||||
- ["github\\.event\\.commits\\[[0-9]+\\]\\.message", "text"]
|
||||
- ["github\\.event\\.head_commit\\.message", "text"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.message", "text"]
|
||||
- ["github\\.event\\.pull_request\\.head\\.repo\\.description", "text"]
|
||||
- ["github\\.event\\.workflow_run\\.head_repository\\.description", "text"]
|
||||
- ["github\\.event\\.changes\\.body\\.from", "title"]
|
||||
# BRANCH
|
||||
- ["github\\.event\\.pull_request\\.head\\.repo\\.default_branch", "branch"]
|
||||
- ["github\\.event\\.pull_request\\.head\\.ref", "branch"]
|
||||
- ["github\\.event\\.workflow_run\\.head_branch", "branch"]
|
||||
- ["github\\.event\\.workflow_run\\.pull_requests\\[[0-9]+\\]\\.head\\.ref", "branch"]
|
||||
- ["github\\.event\\.merge_group\\.head_ref", "branch"]
|
||||
- ["github\\.event\\.changes\\.head\\.ref\\.from", "branch"]
|
||||
# LABEL
|
||||
- ["github\\.event\\.pull_request\\.head\\.label", "label"]
|
||||
# EMAIL
|
||||
- ["github\\.event\\.head_commit\\.author\\.email", "email"]
|
||||
- ["github\\.event\\.head_commit\\.committer\\.email", "email"]
|
||||
- ["github\\.event\\.commits\\[[0-9]+\\]\\.author\\.email", "email"]
|
||||
- ["github\\.event\\.commits\\[[0-9]+\\]\\.committer\\.email", "email"]
|
||||
- ["github\\.event\\.merge_group\\.committer\\.email", "email"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.author\\.email", "email"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.committer\\.email", "email"]
|
||||
# USERNAME
|
||||
- ["github\\.event\\.head_commit\\.author\\.name", "username"]
|
||||
- ["github\\.event\\.head_commit\\.committer\\.name", "username"]
|
||||
- ["github\\.event\\.commits\\[[0-9]+\\]\\.author\\.name", "username"]
|
||||
- ["github\\.event\\.commits\\[[0-9]+\\]\\.committer\\.name", "username"]
|
||||
- ["github\\.event\\.merge_group\\.committer\\.name", "username"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.author\\.name", "username"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.committer\\.name", "username"]
|
||||
# PATH
|
||||
- ["github\\.event\\.workflow\\.path", "path"]
|
||||
- ["github\\.event\\.workflow_run\\.path", "path"]
|
||||
- ["github\\.event\\.workflow_run\\.referenced_workflows\\.path", "path"]
|
||||
# JSON
|
||||
- ["github", "json"]
|
||||
- ["github\\.event", "json"]
|
||||
- ["github\\.event\\.comment", "json"]
|
||||
- ["github\\.event\\.commits", "json"]
|
||||
- ["github\\.event\\.discussion", "json"]
|
||||
- ["github\\.event\\.head_commit", "json"]
|
||||
- ["github\\.event\\.head_commit\\.author", "json"]
|
||||
- ["github\\.event\\.head_commit\\.committer", "json"]
|
||||
- ["github\\.event\\.issue", "json"]
|
||||
- ["github\\.event\\.merge_group", "json"]
|
||||
- ["github\\.event\\.merge_group\\.committer", "json"]
|
||||
- ["github\\.event\\.pull_request", "json"]
|
||||
- ["github\\.event\\.pull_request\\.head", "json"]
|
||||
- ["github\\.event\\.pull_request\\.head\\.repo", "json"]
|
||||
- ["github\\.event\\.pages", "json"]
|
||||
- ["github\\.event\\.review", "json"]
|
||||
- ["github\\.event\\.workflow", "json"]
|
||||
- ["github\\.event\\.workflow_run", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.head_branch", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.author", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.head_commit\\.committer", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.head_repository", "json"]
|
||||
- ["github\\.event\\.workflow_run\\.pull_requests", "json"]
|
||||
- ["github\\.event\\.changes", "json"]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user