diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..dbc72d081 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,173 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Workflow: CodeQL analysis on pull_request, push to main, merge_group, and on demand. +# +# Runs CodeQL via Bazel on relevant C++ targets under //score/... and uploads +# the resulting SARIF file to GitHub Code Scanning so that: +# - CodeQL is executed on every PR (acceptance criteria). +# - PR and merge_group runs use a smart target-picker based on changed files. +# - Pushes to main and manual runs always scan full //score/... for baseline. +# - GitHub tracks findings over time with a stable baseline on main. +# +# Reference: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github + +name: CodeQL Analysis + +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [main] + merge_group: + types: [checks_requested] + workflow_dispatch: # Allow maintainers to trigger manually when needed + +permissions: + contents: read + security-events: write # Required to upload SARIF results to GitHub Code Scanning + +concurrency: + group: codeql-${{ github.run_id }} + cancel-in-progress: false # Never cancel an in-progress CodeQL run; it takes hours + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + codeql: + runs-on: ubuntu-24.04 + timeout-minutes: 340 # CodeQL full scan is long; cap below GitHub's 6-hour hard limit + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + + - name: Setup Bazel + uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: "codeql" + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Pick CodeQL targets + id: targets + run: | + set -euo pipefail + + DEFAULT_TARGET="//score/..." + EVENT_NAME="${GITHUB_EVENT_NAME}" + TARGET="${DEFAULT_TARGET}" + REASON="main_or_manual_full_scan" + + if [[ "${EVENT_NAME}" == "pull_request" || "${EVENT_NAME}" == "merge_group" ]]; then + BASE_BRANCH="${GITHUB_BASE_REF:-main}" + git fetch --no-tags --depth=1 origin "${BASE_BRANCH}" + + CHANGED_FILES="$(git diff --name-only "origin/${BASE_BRANCH}...HEAD" || true)" + + if [[ -z "${CHANGED_FILES}" ]]; then + TARGET="${DEFAULT_TARGET}" + REASON="no_changes_detected_fallback_full_scan" + elif echo "${CHANGED_FILES}" | grep -Eq '^(WORKSPACE|MODULE\.bazel|BUILD|\.bazelrc|bazel/|quality/static_analysis/|third_party/|tools/|\.github/workflows/codeql\.yml)'; then + TARGET="${DEFAULT_TARGET}" + REASON="infra_or_tooling_change_full_scan" + else + mapfile -t SCORE_FILES < <(echo "${CHANGED_FILES}" | grep -E '^score/.*' || true) + + if [[ "${#SCORE_FILES[@]}" -eq 0 ]]; then + TARGET="" + REASON="no_score_changes_skip_scan" + else + : > affected_targets.txt + + for file_path in "${SCORE_FILES[@]}"; do + label="//$(dirname "${file_path}"):$(basename "${file_path}")" + bazel query "kind('.* rule', rdeps(//score/..., ${label}))" 2>/dev/null >> affected_targets.txt || true + done + + sort -u affected_targets.txt -o affected_targets.txt + + if [[ ! -s affected_targets.txt ]]; then + TARGET="${DEFAULT_TARGET}" + REASON="empty_query_result_fallback_full_scan" + else + TARGET_COUNT="$(wc -l < affected_targets.txt | tr -d ' ')" + if [[ "${TARGET_COUNT}" -gt 300 ]]; then + TARGET="${DEFAULT_TARGET}" + REASON="too_many_targets_fallback_full_scan" + else + TARGET="$(tr '\n' ' ' < affected_targets.txt | sed 's/[[:space:]]*$//')" + REASON="smart_target_picker" + fi + fi + fi + fi + fi + + echo "target=${TARGET}" >> "$GITHUB_OUTPUT" + echo "reason=${REASON}" >> "$GITHUB_OUTPUT" + + { + echo "## CodeQL Target Selection" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Event | ${EVENT_NAME} |" + echo "| Reason | ${REASON} |" + + if [[ -z "${TARGET}" ]]; then + echo "| Targets | _none_ (skipped: no score/ changes) |" + elif [[ "${TARGET}" == "${DEFAULT_TARGET}" ]]; then + echo "| Targets | ${DEFAULT_TARGET} |" + else + echo "| Targets | smart-picked subset |" + echo "" + echo "### Picked Targets" + echo '```' + cat affected_targets.txt + echo '```' + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run CodeQL via Bazel + if: ${{ steps.targets.outputs.target != '' }} + run: | + bazel run //quality/static_analysis:codeql_lint -- \ + --target "${{ steps.targets.outputs.target }}" + + - name: Locate SARIF output + id: sarif + if: ${{ steps.targets.outputs.target != '' }} + run: | + SARIF_PATH="$(bazel info output_path)/codeql.sarif" + echo "path=${SARIF_PATH}" >> "$GITHUB_OUTPUT" + + - name: Upload SARIF to GitHub Code Scanning + if: ${{ steps.targets.outputs.target != '' }} + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.sarif.outputs.path }} + category: codeql-bazel