mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
Merge branch 'main' into shared/add-location-to-typetracking-nodes
This commit is contained in:
11
.bazelrc
11
.bazelrc
@@ -1,5 +1,4 @@
|
||||
common --enable_platform_specific_config
|
||||
common --enable_bzlmod
|
||||
# because we use --override_module with `%workspace%`, the lock file is not stable
|
||||
common --lockfile_mode=off
|
||||
|
||||
@@ -24,7 +23,13 @@ common --registry=file:///%workspace%/misc/bazel/registry
|
||||
common --registry=https://bcr.bazel.build
|
||||
|
||||
common --@rules_dotnet//dotnet/settings:strict_deps=false
|
||||
common --experimental_isolated_extension_usages
|
||||
common --incompatible_use_plus_in_repo_names
|
||||
|
||||
# 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
|
||||
|
||||
try-import %workspace%/local.bazelrc
|
||||
|
||||
@@ -8,4 +8,3 @@ 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 @@
|
||||
5f5d70b6c4d2fb1a889479569107f1692239e8a7
|
||||
8.0.0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"bungcip.better-toml",
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -86,4 +86,5 @@
|
||||
/misc/ripunzip/ripunzip-* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# swift prebuilt resources
|
||||
/swift/third_party/resource-dir/*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
/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
|
||||
|
||||
1
.github/codeql/codeql-config.yml
vendored
1
.github/codeql/codeql-config.yml
vendored
@@ -9,3 +9,4 @@ paths-ignore:
|
||||
- '/python/'
|
||||
- '/javascript/ql/test'
|
||||
- '/javascript/extractor/tests'
|
||||
- '/rust/ql'
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -38,6 +38,10 @@ Swift:
|
||||
- swift/**/*
|
||||
- change-notes/**/*swift*
|
||||
|
||||
Actions:
|
||||
- actions/**/*
|
||||
- change-notes/**/*actions*
|
||||
|
||||
documentation:
|
||||
- "**/*.qhelp"
|
||||
- "**/*.md"
|
||||
|
||||
2
.github/workflows/build-ripunzip.yml
vendored
2
.github/workflows/build-ripunzip.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-12, windows-2019]
|
||||
os: [ubuntu-20.04, macos-13, windows-2019]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
3
.github/workflows/check-qldoc.yml
vendored
3
.github/workflows/check-qldoc.yml
vendored
@@ -30,7 +30,8 @@ jobs:
|
||||
run: |
|
||||
EXIT_CODE=0
|
||||
# TODO: remove the shared exception from the regex when coverage of qlpacks without dbschemes is supported
|
||||
changed_lib_packs="$(git diff --name-only --diff-filter=ACMRT HEAD^ HEAD | { grep -Po '^(?!(shared))[a-z]*/ql/lib' || true; } | sort -u)"
|
||||
# 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)"
|
||||
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: 8.0.101
|
||||
dotnet-version: 9.0.100
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
9
.github/workflows/compile-queries.yml
vendored
9
.github/workflows/compile-queries.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
- "rc/*"
|
||||
- "codeql-cli-*"
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.ql'
|
||||
- '**.qll'
|
||||
- '**/qlpack.yml'
|
||||
- '**.dbscheme'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -33,9 +38,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
|
||||
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
|
||||
- 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
|
||||
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
|
||||
|
||||
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-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -38,17 +38,15 @@ jobs:
|
||||
languages: cpp
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: "[Ubuntu] Remove GCC 13 from runner image"
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
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 --allow-downgrades libc6=2.35-* libc6-dev=2.35-* libstdc++6=12.3.0-* libgcc-s1=12.3.0-*
|
||||
sudo apt-get install -y uuid-dev
|
||||
|
||||
- name: "Build Swift extractor using Bazel"
|
||||
run: |
|
||||
bazel clean --expunge
|
||||
bazel run //swift:create-extractor-pack --nouse_action_cache --noremote_accept_cached --noremote_upload_local_results --spawn_strategy=local
|
||||
bazel run //swift:install --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,8 +5,10 @@ on:
|
||||
paths:
|
||||
- "csharp/**"
|
||||
- "shared/**"
|
||||
- "misc/bazel/**"
|
||||
- .github/actions/fetch-codeql/action.yml
|
||||
- codeql-workspace.yml
|
||||
- "MODULE.bazel"
|
||||
branches:
|
||||
- main
|
||||
- "rc/*"
|
||||
@@ -14,9 +16,11 @@ 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/*"
|
||||
@@ -39,14 +43,14 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.101
|
||||
dotnet-version: 9.0.100
|
||||
- name: Extractor unit tests
|
||||
run: |
|
||||
dotnet tool restore
|
||||
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
|
||||
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
|
||||
shell: bash
|
||||
stubgentest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/csv-coverage-metrics.yml
vendored
4
.github/workflows/csv-coverage-metrics.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
DATABASE="${{ runner.temp }}/java-database"
|
||||
codeql database analyze --format=sarif-latest --output=metrics-java.sarif -- "$DATABASE" ./java/ql/src/Metrics/Summaries/FrameworkCoverage.ql
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: metrics-java.sarif
|
||||
path: metrics-java.sarif
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
run: |
|
||||
DATABASE="${{ runner.temp }}/csharp-database"
|
||||
codeql database analyze --format=sarif-latest --output=metrics-csharp.sarif -- "$DATABASE" ./csharp/ql/src/Metrics/Summaries/FrameworkCoverage.ql
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: metrics-csharp.sarif
|
||||
path: metrics-csharp.sarif
|
||||
|
||||
10
.github/workflows/csv-coverage-pr-artifacts.yml
vendored
10
.github/workflows/csv-coverage-pr-artifacts.yml
vendored
@@ -71,21 +71,21 @@ jobs:
|
||||
run: |
|
||||
python base/misc/scripts/library-coverage/compare-folders.py out_base out_merge comparison.md
|
||||
- name: Upload CSV package list
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: csv-framework-coverage-merge
|
||||
path: |
|
||||
out_merge/framework-coverage-*.csv
|
||||
out_merge/framework-coverage-*.rst
|
||||
- name: Upload CSV package list
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: csv-framework-coverage-base
|
||||
path: |
|
||||
out_base/framework-coverage-*.csv
|
||||
out_base/framework-coverage-*.rst
|
||||
- name: Upload comparison results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comparison
|
||||
path: |
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
- name: Upload PR number
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr
|
||||
path: pr/
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
- name: Upload comment ID (if it exists)
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
path: comment/
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
python script/misc/scripts/library-coverage/generate-timeseries.py codeqlModels
|
||||
- name: Upload timeseries CSV
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: framework-coverage-timeseries
|
||||
path: framework-coverage-timeseries-*.csv
|
||||
|
||||
4
.github/workflows/csv-coverage.yml
vendored
4
.github/workflows/csv-coverage.yml
vendored
@@ -34,12 +34,12 @@ jobs:
|
||||
run: |
|
||||
python script/misc/scripts/library-coverage/generate-report.py ci codeqlModels script
|
||||
- name: Upload CSV package list
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: framework-coverage-csv
|
||||
path: framework-coverage-*.csv
|
||||
- name: Upload RST package list
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: framework-coverage-rst
|
||||
path: framework-coverage-*.rst
|
||||
|
||||
16
.github/workflows/mad_modelDiff.yml
vendored
16
.github/workflows/mad_modelDiff.yml
vendored
@@ -38,14 +38,20 @@ jobs:
|
||||
path: codeql-main
|
||||
ref: main
|
||||
- uses: ./codeql-main/.github/actions/fetch-codeql
|
||||
# compute the shortname of the project that does not contain any special (disk) characters
|
||||
- run: |
|
||||
echo "SHORTNAME=${SLUG//[^a-zA-Z0-9_]/}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
SLUG: ${{ matrix.slug }}
|
||||
id: shortname
|
||||
- name: Download database
|
||||
env:
|
||||
SLUG: ${{ matrix.slug }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SHORTNAME: ${{ steps.shortname.outputs.SHORTNAME }}
|
||||
run: |
|
||||
set -x
|
||||
mkdir lib-dbs
|
||||
SHORTNAME=${SLUG//[^a-zA-Z0-9_]/}
|
||||
gh api -H "Accept: application/zip" "/repos/${SLUG}/code-scanning/codeql/databases/java" > "$SHORTNAME.zip"
|
||||
unzip -q -d "${SHORTNAME}-db" "${SHORTNAME}.zip"
|
||||
mkdir "lib-dbs/$SHORTNAME/"
|
||||
@@ -93,14 +99,14 @@ jobs:
|
||||
name="diff_${basename/.model.yml/""}"
|
||||
(diff -w -u $m $t | diff2html -i stdin -F $MODELS/$name.html) || true
|
||||
done
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: models
|
||||
name: models-${{ steps.shortname.outputs.SHORTNAME }}
|
||||
path: tmp-models/**/**/*.model.yml
|
||||
retention-days: 20
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: diffs
|
||||
name: diffs-${{ steps.shortname.outputs.SHORTNAME }}
|
||||
path: tmp-models/*.html
|
||||
# An html file is only produced if the generated models differ.
|
||||
if-no-files-found: ignore
|
||||
|
||||
2
.github/workflows/mad_regenerate-models.yml
vendored
2
.github/workflows/mad_regenerate-models.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
find java -name "*.model.yml" -print0 | xargs -0 git add
|
||||
git status
|
||||
git diff --cached > models.patch
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch
|
||||
path: models.patch
|
||||
|
||||
7
.github/workflows/post-pr-comment.yml
vendored
7
.github/workflows/post-pr-comment.yml
vendored
@@ -17,8 +17,11 @@ jobs:
|
||||
post_comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifact
|
||||
run: gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "comment"
|
||||
- name: Download artifacts
|
||||
run: |
|
||||
gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "comment-pr-number"
|
||||
gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "comment-body"
|
||||
gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "comment-id"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
|
||||
12
.github/workflows/qhelp-pr-preview.yml
vendored
12
.github/workflows/qhelp-pr-preview.yml
vendored
@@ -36,9 +36,9 @@ jobs:
|
||||
- run: echo "${PR_NUMBER}" > pr_number.txt
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
name: comment-pr-number
|
||||
path: pr_number.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
exit "${EXIT_CODE}"
|
||||
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
name: comment-body
|
||||
path: comment_body.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -94,9 +94,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
name: comment-id
|
||||
path: comment_id.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
4
.github/workflows/ql-for-ql-build.yml
vendored
4
.github/workflows/ql-for-ql-build.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
sarif_file: ql-for-ql.sarif
|
||||
category: ql-for-ql
|
||||
- name: Sarif as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ql-for-ql.sarif
|
||||
path: ql-for-ql.sarif
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
mkdir split-sarif
|
||||
node ./ql/scripts/split-sarif.js ql-for-ql.sarif split-sarif
|
||||
- name: Upload langs as artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ql-for-ql-langs
|
||||
path: split-sarif
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
"${CODEQL}" dataset measure --threads 4 --output "stats/${{ matrix.repo }}/stats.xml" "${{ runner.temp }}/database/db-ql"
|
||||
env:
|
||||
CODEQL: ${{ steps.find-codeql.outputs.codeql-path }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: measurements
|
||||
path: stats
|
||||
@@ -76,14 +76,14 @@ jobs:
|
||||
needs: measure
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: measurements
|
||||
path: stats
|
||||
- run: |
|
||||
python -m pip install --user lxml
|
||||
find stats -name 'stats.xml' -print0 | sort -z | xargs -0 python ruby/scripts/merge_stats.py --output ql/ql/src/ql.dbscheme.stats --normalise ql_tokeninfo
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ql.dbscheme.stats
|
||||
path: ql/ql/src/ql.dbscheme.stats
|
||||
|
||||
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 --all -- --check
|
||||
run: cd ql; cargo fmt -- --check
|
||||
- name: Build extractor
|
||||
run: |
|
||||
cd ql;
|
||||
|
||||
2
.github/workflows/query-list.yml
vendored
2
.github/workflows/query-list.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
python codeql/misc/scripts/generate-code-scanning-query-list.py > code-scanning-query-list.csv
|
||||
- name: Upload code scanning query list
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-scanning-query-list
|
||||
path: code-scanning-query-list.csv
|
||||
|
||||
26
.github/workflows/ruby-build.yml
vendored
26
.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 --all -- --check
|
||||
run: cd extractor && cargo fmt -- --check
|
||||
- name: Build
|
||||
if: steps.cache-extractor.outputs.cache-hit != 'true'
|
||||
run: cd extractor && cargo build --verbose
|
||||
@@ -92,17 +92,17 @@ jobs:
|
||||
- name: Generate dbscheme
|
||||
if: ${{ matrix.os == 'ubuntu-latest' && steps.cache-extractor.outputs.cache-hit != 'true'}}
|
||||
run: ../target/release/codeql-extractor-ruby generate --dbscheme ql/lib/ruby.dbscheme --library ql/lib/codeql/ruby/ast/internal/TreeSitter.qll
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
with:
|
||||
name: ruby.dbscheme
|
||||
path: ruby/ql/lib/ruby.dbscheme
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
with:
|
||||
name: TreeSitter.qll
|
||||
path: ruby/ql/lib/codeql/ruby/ast/internal/TreeSitter.qll
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: extractor-${{ matrix.os }}
|
||||
path: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
PACK_FOLDER=$(readlink -f "$PACKS"/codeql/ruby-queries/*)
|
||||
codeql generate query-help --format=sarifv2.1.0 --output="${PACK_FOLDER}/rules.sarif" ql/src
|
||||
(cd ql/src; find queries \( -name '*.qhelp' -o -name '*.rb' -o -name '*.erb' \) -exec bash -c 'mkdir -p "'"${PACK_FOLDER}"'/$(dirname "{}")"' \; -exec cp "{}" "${PACK_FOLDER}/{}" \;)
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codeql-ruby-queries
|
||||
path: |
|
||||
@@ -147,19 +147,19 @@ jobs:
|
||||
needs: [build, compile-queries]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ruby.dbscheme
|
||||
path: ruby/ruby
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: extractor-ubuntu-latest
|
||||
path: ruby/linux64
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: extractor-windows-latest
|
||||
path: ruby/win64
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: extractor-macos-latest
|
||||
path: ruby/osx64
|
||||
@@ -172,13 +172,13 @@ jobs:
|
||||
cp win64/codeql-extractor-ruby.exe ruby/tools/win64/extractor.exe
|
||||
chmod +x ruby/tools/{linux64,osx64}/extractor
|
||||
zip -rq codeql-ruby.zip ruby
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codeql-ruby-pack
|
||||
path: ruby/codeql-ruby.zip
|
||||
retention-days: 1
|
||||
include-hidden-files: true
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: codeql-ruby-queries
|
||||
path: ruby/qlpacks
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
]
|
||||
}' > .codeqlmanifest.json
|
||||
zip -rq codeql-ruby-bundle.zip .codeqlmanifest.json ruby qlpacks
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codeql-ruby-bundle
|
||||
path: ruby/codeql-ruby-bundle.zip
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
uses: ./.github/actions/fetch-codeql
|
||||
|
||||
- name: Download Ruby bundle
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: codeql-ruby-bundle
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
9
.github/workflows/ruby-dataset-measure.yml
vendored
9
.github/workflows/ruby-dataset-measure.yml
vendored
@@ -52,9 +52,9 @@ jobs:
|
||||
run: |
|
||||
mkdir -p "stats/${{ matrix.repo }}"
|
||||
codeql dataset measure --threads 4 --output "stats/${{ matrix.repo }}/stats.xml" "${{ runner.temp }}/database/db-ruby"
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: measurements
|
||||
name: measurements-${{ hashFiles('stats/**') }}
|
||||
path: stats
|
||||
retention-days: 1
|
||||
|
||||
@@ -63,14 +63,13 @@ jobs:
|
||||
needs: measure
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: measurements
|
||||
path: stats
|
||||
- run: |
|
||||
python -m pip install --user lxml
|
||||
find stats -name 'stats.xml' | sort | xargs python ruby/scripts/merge_stats.py --output ruby/ql/lib/ruby.dbscheme.stats --normalise ruby_tokeninfo
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruby.dbscheme.stats
|
||||
path: ruby/ql/lib/ruby.dbscheme.stats
|
||||
|
||||
64
.github/workflows/rust-analysis.yml
vendored
Normal file
64
.github/workflows/rust-analysis.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: "Code scanning - Rust"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'rc/*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'rc/*'
|
||||
paths:
|
||||
- '**/*.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '.github/codeql/codeql-config.yml'
|
||||
- '.github/workflows/rust-analysis.yml'
|
||||
schedule:
|
||||
- cron: '0 9 * * 1'
|
||||
|
||||
env:
|
||||
CODEQL_ENABLE_EXPERIMENTAL_FEATURES: "true"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
strategy:
|
||||
matrix:
|
||||
language: [ 'rust' ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Query latest nightly CodeQL bundle
|
||||
shell: bash
|
||||
id: codeql
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
REPO=dsp-testing/codeql-cli-nightlies
|
||||
TAG=$(
|
||||
gh release list -R $REPO -L1 --exclude-drafts --json tagName -q ".[] | .tagName"
|
||||
)
|
||||
echo "nightly_bundle=https://github.com/$REPO/releases/download/$TAG/codeql-bundle-linux64.tar.zst" \
|
||||
| tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@main
|
||||
with:
|
||||
tools: ${{ steps.codeql.outputs.nightly_bundle }}
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@main
|
||||
34
.github/workflows/rust.yml
vendored
34
.github/workflows/rust.yml
vendored
@@ -23,26 +23,48 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rust-code:
|
||||
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
|
||||
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 --fix
|
||||
git diff --exit-code
|
||||
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
|
||||
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-codegen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
22
.github/workflows/swift.yml
vendored
22
.github/workflows/swift.yml
vendored
@@ -44,27 +44,20 @@ jobs:
|
||||
# without waiting for the macOS build
|
||||
build-and-test-macos:
|
||||
if: github.repository_owner == 'github'
|
||||
runs-on: macos-12-xl
|
||||
runs-on: macos-13-xlarge
|
||||
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
|
||||
runs-on: ubuntu-22.04
|
||||
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
|
||||
runs-on: macos-12-xl
|
||||
runs-on: macos-13-xlarge
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./swift/actions/run-ql-tests
|
||||
@@ -98,7 +91,7 @@ jobs:
|
||||
- name: Generate C++ files
|
||||
run: |
|
||||
bazel run //swift/codegen:codegen -- --generate=trap,cpp --cpp-output=$PWD/generated-cpp-files
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: swift-generated-cpp-files
|
||||
path: generated-cpp-files/**
|
||||
@@ -109,3 +102,10 @@ 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 --all -- --check
|
||||
run: cargo fmt -- --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/(schema.py$|codegen/|.*/generated/|ql/lib/(rust\.dbscheme$|codeql/rust/elements)|\.generated.list)
|
||||
files: ^misc/codegen/|^rust/(prefix\.dbscheme|schema/|codegen/|.*/generated/|ql/lib/(rust\.dbscheme$|codeql/rust/elements)|\.generated.list)
|
||||
language: system
|
||||
entry: bazel run //rust/codegen -- --quiet
|
||||
pass_filenames: false
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"omnisharp.autoStart": false,
|
||||
"cmake.sourceDirectory": "${workspaceFolder}/swift",
|
||||
"cmake.buildDirectory": "${workspaceFolder}/bazel-cmake-build"
|
||||
"cmake.buildDirectory": "${workspaceFolder}/bazel-cmake-build",
|
||||
"editor.suggest.matchOnWordStartOnly": false
|
||||
}
|
||||
|
||||
88
.vscode/tasks.json
vendored
88
.vscode/tasks.json
vendored
@@ -38,6 +38,94 @@
|
||||
"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 +1,5 @@
|
||||
exports_files(["LICENSE"])
|
||||
exports_files([
|
||||
"LICENSE",
|
||||
"Cargo.lock",
|
||||
"Cargo.toml",
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/actions/ @github/codeql-dynamic
|
||||
/cpp/ @github/codeql-c-analysis
|
||||
/csharp/ @github/codeql-csharp
|
||||
/csharp/autobuilder/Semmle.Autobuild.Cpp @github/codeql-c-extractor
|
||||
@@ -23,7 +24,6 @@
|
||||
/ql/ @github/codeql-ql-for-ql-reviewers
|
||||
|
||||
# Bazel (excluding BUILD.bazel files)
|
||||
WORKSPACE.bazel @github/codeql-ci-reviewers
|
||||
MODULE.bazel @github/codeql-ci-reviewers
|
||||
.bazelversion @github/codeql-ci-reviewers
|
||||
.bazelrc @github/codeql-ci-reviewers
|
||||
@@ -43,3 +43,6 @@ 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
|
||||
|
||||
606
Cargo.lock
generated
606
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ members = [
|
||||
"rust/extractor",
|
||||
"rust/extractor/macros",
|
||||
"rust/ast-generator",
|
||||
"rust/autobuild",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
118
MODULE.bazel
118
MODULE.bazel
@@ -15,60 +15,80 @@ local_path_override(
|
||||
# see https://registry.bazel.build/ for a list of available packages
|
||||
|
||||
bazel_dep(name = "platforms", version = "0.0.10")
|
||||
bazel_dep(name = "rules_go", version = "0.50.0")
|
||||
bazel_dep(name = "rules_go", version = "0.50.1")
|
||||
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.35.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.6.1")
|
||||
bazel_dep(name = "abseil-cpp", version = "20240116.0", repo_name = "absl")
|
||||
bazel_dep(name = "rules_python", version = "0.40.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.3.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.7.1")
|
||||
bazel_dep(name = "abseil-cpp", version = "20240116.1", 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.38.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.15.1")
|
||||
bazel_dep(name = "gazelle", version = "0.40.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.17.4")
|
||||
bazel_dep(name = "googletest", version = "1.14.0.bcr.1")
|
||||
bazel_dep(name = "rules_rust", version = "0.52.2")
|
||||
bazel_dep(name = "zstd", version = "1.5.5.bcr.1")
|
||||
|
||||
bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True)
|
||||
|
||||
# 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",
|
||||
],
|
||||
)
|
||||
use_repo(cp, "py_deps")
|
||||
# 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"
|
||||
|
||||
# deps for ruby+rust, but shortened due to windows file paths
|
||||
r = use_extension(
|
||||
"@rules_rust//crate_universe:extension.bzl",
|
||||
"crate",
|
||||
isolate = True,
|
||||
)
|
||||
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",
|
||||
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",
|
||||
],
|
||||
versions = [RUST_VERSION],
|
||||
)
|
||||
use_repo(rust, "rust_toolchains")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
rust_host_tools = use_extension("@rules_rust//rust:extensions.bzl", "rust_host_tools")
|
||||
|
||||
# Don't download a second toolchain as host toolchain, make sure this is the same version as above
|
||||
# The host toolchain is used for vendoring dependencies.
|
||||
rust_host_tools.host_tools(
|
||||
edition = RUST_EDITION,
|
||||
version = RUST_VERSION,
|
||||
)
|
||||
|
||||
# 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__anyhow-1.0.44", "vendor__cc-1.0.70", "vendor__clap-2.33.3", "vendor__regex-1.5.5", "vendor__smallvec-1.6.1", "vendor__string-interner-0.12.2", "vendor__thiserror-1.0.29", "vendor__tree-sitter-0.20.4", "vendor__tree-sitter-graph-0.7.0")
|
||||
|
||||
# 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.24", "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__log-0.4.22", "vendor__num-traits-0.2.19", "vendor__num_cpus-1.16.0", "vendor__proc-macro2-1.0.92", "vendor__quote-1.0.38", "vendor__ra_ap_base_db-0.0.248", "vendor__ra_ap_cfg-0.0.248", "vendor__ra_ap_hir-0.0.248", "vendor__ra_ap_hir_def-0.0.248", "vendor__ra_ap_hir_expand-0.0.248", "vendor__ra_ap_ide_db-0.0.248", "vendor__ra_ap_intern-0.0.248", "vendor__ra_ap_load-cargo-0.0.248", "vendor__ra_ap_parser-0.0.248", "vendor__ra_ap_paths-0.0.248", "vendor__ra_ap_project_model-0.0.248", "vendor__ra_ap_span-0.0.248", "vendor__ra_ap_stdx-0.0.248", "vendor__ra_ap_syntax-0.0.248", "vendor__ra_ap_vfs-0.0.248", "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__stderrlog-0.6.0", "vendor__syn-2.0.95", "vendor__tracing-0.1.41", "vendor__tracing-subscriber-0.3.19", "vendor__tree-sitter-0.24.5", "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`)
|
||||
http_archive(
|
||||
name = "rust-analyzer-src",
|
||||
build_file = "//rust/ast-generator:BUILD.rust-analyzer-src.bazel",
|
||||
integrity = "sha256-jl4KJmZku+ilMLnuX2NU+qa1v10IauSiDiz23sZo360=",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//rust/ast-generator:patches/rust-analyzer.patch",
|
||||
],
|
||||
strip_prefix = "rust-analyzer-2024-12-16",
|
||||
url = "https://github.com/rust-lang/rust-analyzer/archive/refs/tags/2024-12-16.tar.gz",
|
||||
)
|
||||
use_repo(r, tree_sitter_extractors_deps = "r")
|
||||
|
||||
dotnet = use_extension("@rules_dotnet//dotnet:extensions.bzl", "dotnet")
|
||||
dotnet.toolchain(dotnet_version = "8.0.101")
|
||||
dotnet.toolchain(dotnet_version = "9.0.100")
|
||||
use_repo(dotnet, "dotnet_toolchains")
|
||||
|
||||
register_toolchains("@dotnet_toolchains//:all")
|
||||
@@ -91,10 +111,12 @@ use_repo(
|
||||
swift_deps,
|
||||
"binlog",
|
||||
"picosha2",
|
||||
"swift_prebuilt_darwin_x86_64",
|
||||
"swift_prebuilt_linux",
|
||||
"swift_toolchain_linux",
|
||||
"swift_toolchain_macos",
|
||||
"swift-prebuilt-linux",
|
||||
"swift-prebuilt-linux-download-only",
|
||||
"swift-prebuilt-macos",
|
||||
"swift-prebuilt-macos-download-only",
|
||||
"swift-resource-dir-linux",
|
||||
"swift-resource-dir-macos",
|
||||
)
|
||||
|
||||
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
|
||||
@@ -186,16 +208,6 @@ 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",
|
||||
)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# please use MODULE.bazel to add dependencies
|
||||
# this empty file is required by internal repositories, don't remove it
|
||||
9
actions/BUILD.bazel
Normal file
9
actions/BUILD.bazel
Normal file
@@ -0,0 +1,9 @@
|
||||
load("//misc/bazel:pkg.bzl", "codeql_pack")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
codeql_pack(
|
||||
name = "actions",
|
||||
srcs = ["//actions/extractor"],
|
||||
experimental = True,
|
||||
)
|
||||
10
actions/extractor/BUILD.bazel
Normal file
10
actions/extractor/BUILD.bazel
Normal file
@@ -0,0 +1,10 @@
|
||||
load("//misc/bazel:pkg.bzl", "codeql_pkg_files", "strip_prefix")
|
||||
|
||||
codeql_pkg_files(
|
||||
name = "extractor",
|
||||
srcs = [
|
||||
"codeql-extractor.yml",
|
||||
] + glob(["tools/**"]),
|
||||
strip_prefix = strip_prefix.from_pkg(),
|
||||
visibility = ["//actions:__pkg__"],
|
||||
)
|
||||
44
actions/extractor/codeql-extractor.yml
Normal file
44
actions/extractor/codeql-extractor.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: "actions"
|
||||
aliases: []
|
||||
display_name: "GitHub Actions"
|
||||
version: 0.0.1
|
||||
column_kind: "utf16"
|
||||
unicode_newlines: true
|
||||
build_modes:
|
||||
- none
|
||||
file_coverage_languages: []
|
||||
github_api_languages: []
|
||||
scc_languages: []
|
||||
file_types:
|
||||
- name: workflow
|
||||
display_name: GitHub Actions workflow files
|
||||
extensions:
|
||||
- .yml
|
||||
- .yaml
|
||||
forwarded_extractor_name: javascript
|
||||
options:
|
||||
trap:
|
||||
title: TRAP options
|
||||
description: Options about how the extractor handles TRAP files
|
||||
type: object
|
||||
visibility: 3
|
||||
properties:
|
||||
cache:
|
||||
title: TRAP cache options
|
||||
description: Options about how the extractor handles its TRAP cache
|
||||
type: object
|
||||
properties:
|
||||
dir:
|
||||
title: TRAP cache directory
|
||||
description: The directory of the TRAP cache to use
|
||||
type: string
|
||||
bound:
|
||||
title: TRAP cache bound
|
||||
description: A soft limit (in MB) on the size of the TRAP cache
|
||||
type: string
|
||||
pattern: "[0-9]+"
|
||||
write:
|
||||
title: TRAP cache writeable
|
||||
description: Whether to write to the TRAP cache as well as reading it
|
||||
type: string
|
||||
pattern: "(true|TRUE|false|FALSE)"
|
||||
40
actions/extractor/tools/autobuild-impl.ps1
Normal file
40
actions/extractor/tools/autobuild-impl.ps1
Normal file
@@ -0,0 +1,40 @@
|
||||
if (($null -ne $env:LGTM_INDEX_INCLUDE) -or ($null -ne $env:LGTM_INDEX_EXCLUDE) -or ($null -ne $env:LGTM_INDEX_FILTERS)) {
|
||||
Write-Output 'Path filters set. Passing them through to the JavaScript extractor.'
|
||||
} else {
|
||||
Write-Output 'No path filters set. Using the default filters.'
|
||||
$DefaultPathFilters = @(
|
||||
'exclude:**/*',
|
||||
'include:.github/workflows/**/*.yml',
|
||||
'include:.github/workflows/**/*.yaml',
|
||||
'include:**/action.yml',
|
||||
'include:**/action.yaml'
|
||||
)
|
||||
|
||||
$env:LGTM_INDEX_FILTERS = $DefaultPathFilters -join "`n"
|
||||
}
|
||||
|
||||
# Find the JavaScript extractor directory via `codeql resolve extractor`.
|
||||
$CodeQL = Join-Path $env:CODEQL_DIST 'codeql.exe'
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_ROOT = &$CodeQL resolve extractor --language javascript
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Failed to resolve JavaScript extractor.'
|
||||
}
|
||||
|
||||
Write-Output "Found JavaScript extractor at '${env:CODEQL_EXTRACTOR_JAVASCRIPT_ROOT}'."
|
||||
|
||||
# Run the JavaScript autobuilder.
|
||||
$JavaScriptAutoBuild = Join-Path $env:CODEQL_EXTRACTOR_JAVASCRIPT_ROOT 'tools\autobuild.cmd'
|
||||
Write-Output "Running JavaScript autobuilder at '${JavaScriptAutoBuild}'."
|
||||
|
||||
# Copy the values of the Actions extractor environment variables to the JavaScript extractor environment variables.
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_DIAGNOSTIC_DIR = $env:CODEQL_EXTRACTOR_ACTIONS_DIAGNOSTIC_DIR
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_LOG_DIR = $env:CODEQL_EXTRACTOR_ACTIONS_LOG_DIR
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR = $env:CODEQL_EXTRACTOR_ACTIONS_SCRATCH_DIR
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_SOURCE_ARCHIVE_DIR = $env:CODEQL_EXTRACTOR_ACTIONS_SOURCE_ARCHIVE_DIR
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_TRAP_DIR = $env:CODEQL_EXTRACTOR_ACTIONS_TRAP_DIR
|
||||
$env:CODEQL_EXTRACTOR_JAVASCRIPT_WIP_DATABASE = $env:CODEQL_EXTRACTOR_ACTIONS_WIP_DATABASE
|
||||
|
||||
&$JavaScriptAutoBuild
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "JavaScript autobuilder failed."
|
||||
}
|
||||
3
actions/extractor/tools/autobuild.cmd
Normal file
3
actions/extractor/tools/autobuild.cmd
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
rem All of the work is done in the PowerShell script
|
||||
powershell.exe %~dp0autobuild-impl.ps1
|
||||
39
actions/extractor/tools/autobuild.sh
Executable file
39
actions/extractor/tools/autobuild.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
DEFAULT_PATH_FILTERS=$(cat << END
|
||||
exclude:**/*
|
||||
include:.github/workflows/**/*.yml
|
||||
include:.github/workflows/**/*.yaml
|
||||
include:**/action.yml
|
||||
include:**/action.yaml
|
||||
END
|
||||
)
|
||||
|
||||
if [ -n "${LGTM_INDEX_INCLUDE:-}" ] || [ -n "${LGTM_INDEX_EXCLUDE:-}" ] || [ -n "${LGTM_INDEX_FILTERS:-}" ] ; then
|
||||
echo "Path filters set. Passing them through to the JavaScript extractor."
|
||||
else
|
||||
echo "No path filters set. Using the default filters."
|
||||
LGTM_INDEX_FILTERS="${DEFAULT_PATH_FILTERS}"
|
||||
export LGTM_INDEX_FILTERS
|
||||
fi
|
||||
|
||||
# Find the JavaScript extractor directory via `codeql resolve extractor`.
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_ROOT="$($CODEQL_DIST/codeql resolve extractor --language javascript)"
|
||||
export CODEQL_EXTRACTOR_JAVASCRIPT_ROOT
|
||||
|
||||
echo "Found JavaScript extractor at '${CODEQL_EXTRACTOR_JAVASCRIPT_ROOT}'."
|
||||
|
||||
# Run the JavaScript autobuilder
|
||||
JAVASCRIPT_AUTO_BUILD="${CODEQL_EXTRACTOR_JAVASCRIPT_ROOT}/tools/autobuild.sh"
|
||||
echo "Running JavaScript autobuilder at '${JAVASCRIPT_AUTO_BUILD}'."
|
||||
|
||||
# Copy the values of the Actions extractor environment variables to the JavaScript extractor environment variables.
|
||||
env CODEQL_EXTRACTOR_JAVASCRIPT_DIAGNOSTIC_DIR="${CODEQL_EXTRACTOR_ACTIONS_DIAGNOSTIC_DIR}" \
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_LOG_DIR="${CODEQL_EXTRACTOR_ACTIONS_LOG_DIR}" \
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_SCRATCH_DIR="${CODEQL_EXTRACTOR_ACTIONS_SCRATCH_DIR}" \
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_SOURCE_ARCHIVE_DIR="${CODEQL_EXTRACTOR_ACTIONS_SOURCE_ARCHIVE_DIR}" \
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_TRAP_DIR="${CODEQL_EXTRACTOR_ACTIONS_TRAP_DIR}" \
|
||||
CODEQL_EXTRACTOR_JAVASCRIPT_WIP_DATABASE="${CODEQL_EXTRACTOR_ACTIONS_WIP_DATABASE}" \
|
||||
${JAVASCRIPT_AUTO_BUILD}
|
||||
1
actions/ql/lib/actions.qll
Normal file
1
actions/ql/lib/actions.qll
Normal file
@@ -0,0 +1 @@
|
||||
import codeql.actions.Ast
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: feature
|
||||
---
|
||||
* Initial public preview release
|
||||
4
actions/ql/lib/codeql-pack.lock.yml
Normal file
4
actions/ql/lib/codeql-pack.lock.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
lockVersion: 1.0.0
|
||||
dependencies: {}
|
||||
compiled: false
|
||||
98
actions/ql/lib/codeql/Locations.qll
Normal file
98
actions/ql/lib/codeql/Locations.qll
Normal file
@@ -0,0 +1,98 @@
|
||||
/** 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) }
|
||||
}
|
||||
400
actions/ql/lib/codeql/actions/Ast.qll
Normal file
400
actions/ql/lib/codeql/actions/Ast.qll
Normal file
@@ -0,0 +1,400 @@
|
||||
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 { }
|
||||
722
actions/ql/lib/codeql/actions/Bash.qll
Normal file
722
actions/ql/lib/codeql/actions/Bash.qll
Normal file
@@ -0,0 +1,722 @@
|
||||
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]", "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
// 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+\\})\\$$" }
|
||||
}
|
||||
6
actions/ql/lib/codeql/actions/Cfg.qll
Normal file
6
actions/ql/lib/codeql/actions/Cfg.qll
Normal file
@@ -0,0 +1,6 @@
|
||||
/** 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
actions/ql/lib/codeql/actions/Consistency.ql
Normal file
1
actions/ql/lib/codeql/actions/Consistency.ql
Normal file
@@ -0,0 +1 @@
|
||||
import DataFlow::DataFlow::Consistency
|
||||
22
actions/ql/lib/codeql/actions/DataFlow.qll
Normal file
22
actions/ql/lib/codeql/actions/DataFlow.qll
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
}
|
||||
88
actions/ql/lib/codeql/actions/Helper.qll
Normal file
88
actions/ql/lib/codeql/actions/Helper.qll
Normal file
@@ -0,0 +1,88 @@
|
||||
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 }
|
||||
62
actions/ql/lib/codeql/actions/PowerShell.qll
Normal file
62
actions/ql/lib/codeql/actions/PowerShell.qll
Normal file
@@ -0,0 +1,62 @@
|
||||
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() }
|
||||
}
|
||||
13
actions/ql/lib/codeql/actions/TaintTracking.qll
Normal file
13
actions/ql/lib/codeql/actions/TaintTracking.qll
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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"
|
||||
)
|
||||
}
|
||||
1924
actions/ql/lib/codeql/actions/ast/internal/Ast.qll
Normal file
1924
actions/ql/lib/codeql/actions/ast/internal/Ast.qll
Normal file
File diff suppressed because it is too large
Load Diff
57
actions/ql/lib/codeql/actions/ast/internal/Yaml.qll
Normal file
57
actions/ql/lib/codeql/actions/ast/internal/Yaml.qll
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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>
|
||||
147
actions/ql/lib/codeql/actions/config/Config.qll
Normal file
147
actions/ql/lib/codeql/actions/config/Config.qll
Normal file
@@ -0,0 +1,147 @@
|
||||
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 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)
|
||||
}
|
||||
74
actions/ql/lib/codeql/actions/config/ConfigExtensions.qll
Normal file
74
actions/ql/lib/codeql/actions/config/ConfigExtensions.qll
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 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);
|
||||
444
actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll
Normal file
444
actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll
Normal file
@@ -0,0 +1,444 @@
|
||||
/** 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) }
|
||||
}
|
||||
316
actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll
Normal file
316
actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll
Normal file
@@ -0,0 +1,316 @@
|
||||
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() }
|
||||
}
|
||||
|
||||
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 { }
|
||||
131
actions/ql/lib/codeql/actions/dataflow/ExternalFlow.qll
Normal file
131
actions/ql/lib/codeql/actions/dataflow/ExternalFlow.qll
Normal file
@@ -0,0 +1,131 @@
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
366
actions/ql/lib/codeql/actions/dataflow/FlowSources.qll
Normal file
366
actions/ql/lib/codeql/actions/dataflow/FlowSources.qll
Normal file
@@ -0,0 +1,366 @@
|
||||
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() }
|
||||
}
|
||||
92
actions/ql/lib/codeql/actions/dataflow/FlowSteps.qll
Normal file
92
actions/ql/lib/codeql/actions/dataflow/FlowSteps.qll
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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()
|
||||
)
|
||||
}
|
||||
156
actions/ql/lib/codeql/actions/dataflow/TaintSteps.qll
Normal file
156
actions/ql/lib/codeql/actions/dataflow/TaintSteps.qll
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
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() }
|
||||
@@ -0,0 +1,194 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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() }
|
||||
@@ -0,0 +1,19 @@
|
||||
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("//", "/")
|
||||
}
|
||||
137
actions/ql/lib/codeql/actions/ideContextual/printAst.qll
Normal file
137
actions/ql/lib/codeql/actions/ideContextual/printAst.qll
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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>;
|
||||
@@ -0,0 +1,322 @@
|
||||
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>;
|
||||
@@ -0,0 +1,72 @@
|
||||
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") }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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>;
|
||||
@@ -0,0 +1,22 @@
|
||||
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>;
|
||||
312
actions/ql/lib/codeql/actions/security/ControlChecks.qll
Normal file
312
actions/ql/lib/codeql/actions/security/ControlChecks.qll
Normal file
@@ -0,0 +1,312 @@
|
||||
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.*")
|
||||
)
|
||||
}
|
||||
}
|
||||
114
actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll
Normal file
114
actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll
Normal file
@@ -0,0 +1,114 @@
|
||||
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>;
|
||||
169
actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll
Normal file
169
actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll
Normal file
@@ -0,0 +1,169 @@
|
||||
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>;
|
||||
220
actions/ql/lib/codeql/actions/security/OutputClobberingQuery.qll
Normal file
220
actions/ql/lib/codeql/actions/security/OutputClobberingQuery.qll
Normal file
@@ -0,0 +1,220 @@
|
||||
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>;
|
||||
56
actions/ql/lib/codeql/actions/security/PoisonableSteps.qll
Normal file
56
actions/ql/lib/codeql/actions/security/PoisonableSteps.qll
Normal file
@@ -0,0 +1,56 @@
|
||||
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()) }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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>;
|
||||
@@ -0,0 +1,21 @@
|
||||
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>;
|
||||
45
actions/ql/lib/codeql/actions/security/SelfHostedQuery.qll
Normal file
45
actions/ql/lib/codeql/actions/security/SelfHostedQuery.qll
Normal file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
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() }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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 }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
177
actions/ql/lib/codeql/files/FileSystem.qll
Normal file
177
actions/ql/lib/codeql/files/FileSystem.qll
Normal file
@@ -0,0 +1,177 @@
|
||||
/** 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() }
|
||||
}
|
||||
15
actions/ql/lib/ext/config/argument_injection_sinks.yml
Normal file
15
actions/ql/lib/ext/config/argument_injection_sinks.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
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]
|
||||
|
||||
53
actions/ql/lib/ext/config/context_event_map.yml
Normal file
53
actions/ql/lib/ext/config/context_event_map.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
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"]
|
||||
|
||||
19
actions/ql/lib/ext/config/externally_triggereable_events.yml
Normal file
19
actions/ql/lib/ext/config/externally_triggereable_events.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
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"]
|
||||
22
actions/ql/lib/ext/config/immutable_actions.yml
Normal file
22
actions/ql/lib/ext/config/immutable_actions.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
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"]
|
||||
- ["octokit/request-action"]
|
||||
76
actions/ql/lib/ext/config/poisonable_steps.yml
Normal file
76
actions/ql/lib/ext/config/poisonable_steps.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
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]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user