From 306f491bfa3fcf476164e902ae196c289bbca0d3 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 18 Dec 2025 18:55:49 -0500 Subject: [PATCH 1/5] github actions: test on complete. WIWT took agressive security recommendations from cluade --- .../validate-kernel-commits-check-secure.yml | 281 +++++++++++ ...validate-kernel-commits-comment-secure.yml | 435 ++++++++++++++++++ 2 files changed, 716 insertions(+) create mode 100644 .github/workflows/validate-kernel-commits-check-secure.yml create mode 100644 .github/workflows/validate-kernel-commits-comment-secure.yml diff --git a/.github/workflows/validate-kernel-commits-check-secure.yml b/.github/workflows/validate-kernel-commits-check-secure.yml new file mode 100644 index 000000000000..28e725b71b2a --- /dev/null +++ b/.github/workflows/validate-kernel-commits-check-secure.yml @@ -0,0 +1,281 @@ +name: Validate Kernel Commits - Check (Secure) + +on: + workflow_call: + # No inputs needed - uses github context from caller + +permissions: + contents: read + # No pull-requests: write needed - we don't comment here + +jobs: + validate-kernel-commits-check: + runs-on: ubuntu-latest + timeout-minutes: 120 + + steps: + - name: Validate and sanitize inputs + id: validate_inputs + env: + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_COMMITS: ${{ github.event.pull_request.commits }} + run: | + # Validate base branch name (alphanumeric, dots, slashes, dashes, underscores, curly braces) + # Note: hyphen must be at end of character class or escaped to be literal + if ! [[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then + echo "❌ Invalid base branch name: $BASE_REF" + exit 1 + fi + + # Validate head branch name + if ! [[ "$HEAD_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then + echo "❌ Invalid head branch name: $HEAD_REF" + exit 1 + fi + + # Validate length (prevent resource exhaustion) + if [ ${#BASE_REF} -gt 255 ]; then + echo "❌ Base branch name too long" + exit 1 + fi + + if [ ${#HEAD_REF} -gt 255 ]; then + echo "❌ Head branch name too long" + exit 1 + fi + + # Validate PR number is numeric + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number: $PR_NUMBER" + exit 1 + fi + + # Validate commits count is numeric + if ! [[ "$PR_COMMITS" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid commits count: $PR_COMMITS" + exit 1 + fi + + # Pass validated values to environment + echo "BASE_REF=$BASE_REF" >> "$GITHUB_ENV" + echo "HEAD_REF=$HEAD_REF" >> "$GITHUB_ENV" + echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" + echo "PR_COMMITS=$PR_COMMITS" >> "$GITHUB_ENV" + + - name: Clone base branch + env: + BASE_CLONE_URL: ${{ github.event.pull_request.base.repo.clone_url }} + run: | + # Use environment variables to prevent injection + git clone --depth=1 --no-checkout "$BASE_CLONE_URL" -b "$BASE_REF" . + + - name: Fetch PR branch + env: + HEAD_CLONE_URL: ${{ github.event.pull_request.head.repo.clone_url }} + run: | + # Use environment variables to prevent command injection + git fetch --depth=$((PR_COMMITS + 1)) "$HEAD_CLONE_URL" "$HEAD_REF" + HEAD_SHA=$(git rev-parse FETCH_HEAD) + + # Validate SHA format (40 hex characters) + if ! [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "❌ Invalid SHA format: $HEAD_SHA" + exit 1 + fi + + echo "HEAD_SHA=$HEAD_SHA" >> "$GITHUB_ENV" + + - name: Verify PR branch isn't on stale base + run: | + if ! git merge-base --is-ancestor "$BASE_REF" "$HEAD_SHA"; then + echo "❌ PR branch must be rebased onto latest base branch commit" + exit 1 + fi + + - name: Fetch upstream mainline branch + run: | + # Determine the kernel version tag, since it is the most recent common + # ancestor between the base branch and the upstream mainline branch + MAKEFILE=$(git show "$BASE_REF":Makefile) + VERSION=$(grep -m1 -Po '(?<=^VERSION = )\d+' <<< "$MAKEFILE") + PATCHLEVEL=$(grep -m1 -Po '(?<=^PATCHLEVEL = )\d+' <<< "$MAKEFILE") + + # Validate VERSION and PATCHLEVEL are numeric + if ! [[ "$VERSION" =~ ^[0-9]+$ ]] || ! [[ "$PATCHLEVEL" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid kernel version: $VERSION.$PATCHLEVEL" + exit 1 + fi + + # Fetch upstream mainline branch without tags, fetching only history + # that is newer than this kernel version tag. This reduces the amount + # of commits cloned knowing that there's no need to check any commits + # for Fixes commits older than what the base branch already contains + git fetch --no-tags --shallow-exclude="v$VERSION.$PATCHLEVEL" origin kernel-mainline + MAINLINE_SHA=$(git rev-parse FETCH_HEAD) + + # Validate SHA format + if ! [[ "$MAINLINE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "❌ Invalid mainline SHA format: $MAINLINE_SHA" + exit 1 + fi + + echo "MAINLINE_SHA=$MAINLINE_SHA" >> "$GITHUB_ENV" + + - name: Checkout kernel-src-tree-tools + uses: actions/checkout@v4 + with: + repository: ctrliq/kernel-src-tree-tools + ref: 'mainline' + path: kernel-src-tree-tools + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Run upstream fixes check + id: check-kernel-commits + working-directory: kernel-src-tree-tools + run: | + set +e # Don't exit on error, we want to capture the output + set -o pipefail # Capture exit code from python script, not tee + python3 check_kernel_commits.py \ + --repo .. \ + --pr_branch "$HEAD_SHA" \ + --base_branch "$BASE_REF" \ + --markdown \ + --upstream-ref "$MAINLINE_SHA" \ + --check-cves | tee ../ckc_result.txt + EXIT_CODE=$? + + # Check if the script failed + if [ $EXIT_CODE -ne 0 ]; then + echo "❌ Kernel commits check failed with exit code $EXIT_CODE" + exit $EXIT_CODE + fi + + # Check for findings: + # 1. Verify the success message exists + # 2. If it exists, check if there are any OTHER lines (which would indicate issues) + # 3. If success message doesn't exist, that's also a finding + if grep -q "All referenced commits exist upstream and have no Fixes: tags." ../ckc_result.txt; then + # Success message found, check if there are any other lines + LINE_COUNT=$(wc -l < ../ckc_result.txt) + if [ "$LINE_COUNT" -gt 1 ]; then + echo "has_findings=true" >> $GITHUB_OUTPUT + else + echo "has_findings=false" >> $GITHUB_OUTPUT + fi + else + # Success message not found, there must be findings + echo "has_findings=true" >> $GITHUB_OUTPUT + fi + + set -e # Re-enable exit on error + + - name: Install build dependencies for patchutils + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool gnulib + + - name: Clone and build custom patchutils + run: | + # Security: Pin to specific commit to prevent supply chain attacks + EXPECTED_COMMIT="60a60b3909d0e29c0ff286f6a73de4168977b097" + + # Clone repository + git clone https://github.com/kerneltoast/patchutils.git + cd patchutils + + # Fetch the specific commit we want + git fetch origin "$EXPECTED_COMMIT" + git checkout "$EXPECTED_COMMIT" + + # Verify we're on the expected commit + ACTUAL_COMMIT=$(git rev-parse HEAD) + if [ "$ACTUAL_COMMIT" != "$EXPECTED_COMMIT" ]; then + echo "❌ Security: Commit mismatch!" + echo "Expected: $EXPECTED_COMMIT" + echo "Actual: $ACTUAL_COMMIT" + exit 1 + fi + + # Build patchutils + ./bootstrap + ./configure + make -j$(nproc) + + # Verify the binary was created + if [ ! -x src/interdiff ]; then + echo "❌ Failed to build interdiff binary" + exit 1 + fi + + - name: Run interdiff check + id: interdiff + working-directory: kernel-src-tree-tools + run: | + set +e # Don't exit on error, we want to capture the output + set -o pipefail # Capture exit code from python script, not tee + python3 run_interdiff.py \ + --repo .. \ + --pr_branch "$HEAD_SHA" \ + --base_branch "$BASE_REF" \ + --markdown \ + --interdiff ../patchutils/src/interdiff | tee ../interdiff_result.txt + EXIT_CODE=$? + + # Check if the script failed + if [ $EXIT_CODE -ne 0 ]; then + echo "❌ Interdiff check failed with exit code $EXIT_CODE" + exit $EXIT_CODE + fi + + # Check for differences: + # 1. Verify the success message exists + # 2. If it exists, check if there are any OTHER lines (which would indicate differences) + # 3. If success message doesn't exist, that's also a difference + if grep -q "All backported commits match their upstream counterparts." ../interdiff_result.txt; then + # Success message found, check if there are any other lines + LINE_COUNT=$(wc -l < ../interdiff_result.txt) + if [ "$LINE_COUNT" -gt 1 ]; then + echo "has_differences=true" >> $GITHUB_OUTPUT + else + echo "has_differences=false" >> $GITHUB_OUTPUT + fi + else + # Success message not found, there must be differences + echo "has_differences=true" >> $GITHUB_OUTPUT + fi + + set -e # Re-enable exit on error + + - name: Save PR metadata for comment workflow + env: + HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} + REPOSITORY: ${{ github.repository }} + run: | + mkdir -p pr_metadata + + # Save validated metadata + echo "$PR_NUMBER" > pr_metadata/pr_number.txt + echo "$REPOSITORY" > pr_metadata/repository.txt + echo "$BASE_REF" > pr_metadata/base_ref.txt + echo "$HEAD_SHA" > pr_metadata/head_sha.txt + echo "$HEAD_REPO_FULL_NAME" > pr_metadata/head_repo.txt + + # Create a checksum of metadata for integrity verification + (cd pr_metadata && sha256sum *.txt > checksums.txt) + + - name: Upload check results + uses: actions/upload-artifact@v4 + if: always() # Upload even if checks fail + with: + name: check-results + path: | + ckc_result.txt + interdiff_result.txt + pr_metadata/ + retention-days: 3 # Increased from 1 to prevent premature deletion diff --git a/.github/workflows/validate-kernel-commits-comment-secure.yml b/.github/workflows/validate-kernel-commits-comment-secure.yml new file mode 100644 index 000000000000..98ed41839d6d --- /dev/null +++ b/.github/workflows/validate-kernel-commits-comment-secure.yml @@ -0,0 +1,435 @@ +name: Validate Kernel Commits - Post Comments (Secure) + +on: + workflow_run: + workflows: ["Validate Kernel Commits"] + types: + - completed + workflow_dispatch: + inputs: + run_id: + description: "Workflow run ID to fetch artifacts from" + required: true + type: string + +permissions: + contents: read + pull-requests: write # Need write to post comments + +jobs: + post-comments: + runs-on: ubuntu-latest + # Only run if the check workflow succeeded or failed (not skipped/cancelled) + # For workflow_dispatch, always run (manual testing) + if: | + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') + + steps: + - name: Download check results + uses: actions/download-artifact@v4 + with: + name: check-results + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }} + + - name: Verify artifact integrity + run: | + if [ ! -f pr_metadata/checksums.txt ]; then + echo "⚠️ Warning: No checksums file found, skipping integrity check" + else + cd pr_metadata + if sha256sum -c checksums.txt --quiet; then + echo "✅ Artifact integrity verified" + else + echo "❌ Artifact integrity check failed!" + exit 1 + fi + cd .. + fi + + - name: Read and validate PR metadata + id: pr_metadata + run: | + if [ ! -f pr_metadata/pr_number.txt ]; then + echo "❌ PR metadata not found - check workflow may have failed before saving metadata" + exit 1 + fi + + # Read values into variables (not step outputs yet - validate first!) + PR_NUMBER=$(cat pr_metadata/pr_number.txt) + REPOSITORY=$(cat pr_metadata/repository.txt) + BASE_REF=$(cat pr_metadata/base_ref.txt) + HEAD_SHA=$(cat pr_metadata/head_sha.txt) + HEAD_REPO=$(cat pr_metadata/head_repo.txt) + + # === CRITICAL VALIDATION: Prevent command injection === + + # Validate PR number is actually a number + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Security: Invalid PR number format: $PR_NUMBER" + exit 1 + fi + + # Validate PR number is reasonable (1 to 7 digits) + if [ "$PR_NUMBER" -lt 1 ] || [ "$PR_NUMBER" -gt 9999999 ]; then + echo "❌ Security: PR number out of range: $PR_NUMBER" + exit 1 + fi + + # Validate repository format (owner/repo) + if ! [[ "$REPOSITORY" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then + echo "❌ Security: Invalid repository format: $REPOSITORY" + exit 1 + fi + + # Validate repository name length + if [ ${#REPOSITORY} -gt 100 ]; then + echo "❌ Security: Repository name too long" + exit 1 + fi + + # Validate SHA is exactly 40 hex characters + if ! [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "❌ Security: Invalid SHA format: $HEAD_SHA" + exit 1 + fi + + # Validate branch name (alphanumeric, dots, slashes, dashes, underscores, curly braces) + if ! [[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then + echo "❌ Security: Invalid base branch name: $BASE_REF" + exit 1 + fi + + # Validate branch name length + if [ ${#BASE_REF} -gt 255 ]; then + echo "❌ Security: Branch name too long" + exit 1 + fi + + # Validate head repo format (can be empty for deleted forks) + if [ -n "$HEAD_REPO" ] && ! [[ "$HEAD_REPO" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then + echo "❌ Security: Invalid head repo format: $HEAD_REPO" + exit 1 + fi + + # === All validation passed - safe to use === + echo "✅ All metadata validation passed" + + # Now safe to output (these will be used in subsequent steps) + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "repository=$REPOSITORY" >> $GITHUB_OUTPUT + echo "base_ref=$BASE_REF" >> $GITHUB_OUTPUT + echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT + echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT + + - name: Check if PR is from fork + id: is_fork + run: | + if [ "${{ steps.pr_metadata.outputs.head_repo }}" = "${{ steps.pr_metadata.outputs.repository }}" ]; then + echo "result=false" >> $GITHUB_OUTPUT + echo "✅ Internal PR detected" + else + echo "result=false" >> $GITHUB_OUTPUT + echo "✅ Fork PR detected - JIRA checks will be skipped" + fi + + - name: Post workflow run link to PR + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + BODY="🤖 **Validation Checks In Progress** \ + \ + Workflow run: $RUN_URL \ + \ + This comment will be updated with check results when complete." + + gh pr comment "$PR_NUMBER" \ + --body "$BODY" \ + --repo "$REPOSITORY" + + - name: Validate comment files + run: | + # Validate result files exist and are reasonable size + if [ -f ckc_result.txt ]; then + FILE_SIZE=$(stat -f%z ckc_result.txt 2>/dev/null || stat -c%s ckc_result.txt 2>/dev/null) + if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit + echo "❌ Security: ckc_result.txt file too large: $FILE_SIZE bytes" + exit 1 + fi + echo "✅ ckc_result.txt validated (size: $FILE_SIZE bytes)" + fi + + if [ -f interdiff_result.txt ]; then + FILE_SIZE=$(stat -f%z interdiff_result.txt 2>/dev/null || stat -c%s interdiff_result.txt 2>/dev/null) + if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit + echo "❌ Security: interdiff_result.txt file too large: $FILE_SIZE bytes" + exit 1 + fi + echo "✅ interdiff_result.txt validated (size: $FILE_SIZE bytes)" + fi + + - name: Comment on PR if check-kernel-commits issues found + if: hashFiles('ckc_result.txt') != '' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + run: | + # Check if there are findings + if grep -q "All referenced commits exist upstream and have no Fixes: tags." ckc_result.txt; then + LINE_COUNT=$(wc -l < ckc_result.txt) + if [ "$LINE_COUNT" -le 1 ]; then + echo "✅ No check-kernel-commits findings, skipping comment" + exit 0 + fi + fi + + # Post comment using environment variables (prevents injection) + if ! gh pr comment "$PR_NUMBER" \ + --body-file ckc_result.txt \ + --repo "$REPOSITORY"; then + echo "❌ Failed to post check-kernel-commits comment to PR" + exit 1 + fi + + echo "✅ Posted check-kernel-commits comment to PR #$PR_NUMBER" + + - name: Comment on PR if interdiff differences found + if: hashFiles('interdiff_result.txt') != '' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + run: | + # Check if there are differences + if grep -q "All backported commits match their upstream counterparts." interdiff_result.txt; then + LINE_COUNT=$(wc -l < interdiff_result.txt) + if [ "$LINE_COUNT" -le 1 ]; then + echo "✅ No interdiff differences, skipping comment" + exit 0 + fi + fi + + # Post comment using environment variables (prevents injection) + if ! gh pr comment "$PR_NUMBER" \ + --body-file interdiff_result.txt \ + --repo "$REPOSITORY"; then + echo "❌ Failed to post interdiff comment to PR" + exit 1 + fi + + echo "✅ Posted interdiff comment to PR #$PR_NUMBER" + + - name: Checkout PR head for JIRA check + if: steps.is_fork.outputs.result == 'false' + env: + CLONE_URL: ${{ github.event_name == 'workflow_dispatch' && format('{0}/{1}.git', github.server_url, github.repository) || github.event.workflow_run.repository.clone_url }} + BASE_REF: ${{ steps.pr_metadata.outputs.base_ref }} + HEAD_SHA: ${{ steps.pr_metadata.outputs.head_sha }} + run: | + # Use environment variables to prevent command injection + # Clone into subdirectory since workspace contains artifacts + git clone --depth=1 --no-checkout "$CLONE_URL" -b "$BASE_REF" kernel-src-tree + cd kernel-src-tree + git fetch --depth=100 origin "$HEAD_SHA" + git checkout "$HEAD_SHA" + + # Create a temporary branch to avoid detached HEAD issues + git checkout -b temp-pr-check + + # Verify we're on the expected commit + ACTUAL_SHA=$(git rev-parse HEAD) + if [ "$ACTUAL_SHA" != "$HEAD_SHA" ]; then + echo "❌ Security: SHA mismatch after checkout!" + echo "Expected: $HEAD_SHA" + echo "Actual: $ACTUAL_SHA" + exit 1 + fi + + echo "✅ Checked out commit $HEAD_SHA to kernel-src-tree/ (on branch temp-pr-check)" + + - name: Checkout kernel-src-tree-tools for JIRA check + if: steps.is_fork.outputs.result == 'false' + uses: actions/checkout@v4 + with: + repository: ctrliq/kernel-src-tree-tools + ref: 'mainline' + path: kernel-src-tree-tools + + - name: Set up Python for JIRA check + if: steps.is_fork.outputs.result == 'false' + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install JIRA PR Check dependencies + if: steps.is_fork.outputs.result == 'false' + run: | + python -m pip install --upgrade pip + pip install jira + + - name: Mask JIRA credentials + if: steps.is_fork.outputs.result == 'false' + run: | + echo "::add-mask::${{ secrets.JIRA_API_TOKEN }}" + echo "::add-mask::${{ secrets.JIRA_API_USER }}" + echo "::add-mask::${{ secrets.JIRA_URL }}" + + - name: Run JIRA PR Check + if: steps.is_fork.outputs.result == 'false' + id: jira_check + continue-on-error: true # Allow PR comments to be posted before failing workflow + env: + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_API_USER: ${{ secrets.JIRA_API_USER }} + JIRA_URL: ${{ secrets.JIRA_URL }} + BASE_REF: ${{ steps.pr_metadata.outputs.base_ref }} + HEAD_SHA: ${{ steps.pr_metadata.outputs.head_sha }} + working-directory: kernel-src-tree-tools + run: | + # Run script and capture output, ensuring credentials are never echoed + set +x # Disable command echo to prevent credential exposure + set +e # Don't exit on error, we want to capture the output + + # Use environment variables for all arguments (prevents injection) + OUTPUT=$(python3 jira_pr_check.py \ + --kernel-src-tree ../kernel-src-tree \ + --merge-target "$BASE_REF" \ + --pr-branch "$HEAD_SHA" 2>&1) + EXIT_CODE=$? + + # Filter out any potential credential leaks from output + # Use fixed string matching to avoid regex issues with special chars in tokens + FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v -F -e "jira-user" -e "jira-key" -e "basic_auth" -e "Authorization" -e "Bearer") + + # Additional safety: remove any lines that might contain the actual token + # Do this without putting the token in the command line + FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v "$JIRA_API_USER" || true) + + echo "$FILTERED_OUTPUT" + + # Save output using heredoc (safer than multiline strings) + { + echo "output<> $GITHUB_OUTPUT + + # Check if there are any issues based on output patterns + if echo "$FILTERED_OUTPUT" | grep -q "❌ Errors:"; then + echo "has_issues=true" >> $GITHUB_OUTPUT + + # Check specifically for LTS mismatch errors + if echo "$FILTERED_OUTPUT" | grep -q "expects branch"; then + echo "has_lts_mismatch=true" >> $GITHUB_OUTPUT + else + echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT + fi + elif echo "$FILTERED_OUTPUT" | grep -q "⚠️ Warnings:"; then + echo "has_issues=true" >> $GITHUB_OUTPUT + echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT + else + echo "has_issues=false" >> $GITHUB_OUTPUT + echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT + fi + + # Exit with the script's exit code + exit $EXIT_CODE + + - name: Comment PR with JIRA issues + if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_issues == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + JIRA_OUTPUT: ${{ steps.jira_check.outputs.output }} + run: | + # Validate output size before posting + OUTPUT_SIZE=${#JIRA_OUTPUT} + if [ "$OUTPUT_SIZE" -gt 65536 ]; then # 64KB limit + echo "⚠️ JIRA output too large ($OUTPUT_SIZE bytes), truncating" + JIRA_OUTPUT="${JIRA_OUTPUT:0:65000}... (output truncated due to size)" + fi + + # Post comment using environment variable (prevents injection) + if ! gh pr comment "$PR_NUMBER" \ + --body "$JIRA_OUTPUT" \ + --repo "$REPOSITORY"; then + echo "❌ Failed to post JIRA check comment to PR" + exit 1 + fi + + echo "✅ Posted JIRA comment to PR #$PR_NUMBER" + + - name: Dismiss previous bot reviews if checks pass + if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outcome == 'success' && steps.jira_check.outputs.has_issues == 'false' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + run: | + # Find and dismiss any previous "changes requested" reviews from github-actions bot + REVIEW_IDS=$(gh api "/repos/$REPOSITORY/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.state == "CHANGES_REQUESTED" and .user.login == "github-actions[bot]") | .id') + + if [ -n "$REVIEW_IDS" ]; then + echo "$REVIEW_IDS" | while read -r REVIEW_ID; do + echo "Dismissing review ID: $REVIEW_ID" + gh api "/repos/$REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" \ + -X PUT \ + -f message="All validation checks now pass. Issues have been resolved." \ + -f event="DISMISS" || echo "⚠️ Failed to dismiss review $REVIEW_ID" + done + echo "✅ Dismissed previous change requests" + else + echo "ℹ️ No previous change requests to dismiss" + fi + + - name: Request changes if LTS mismatch + if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_lts_mismatch == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + run: | + gh pr review "$PR_NUMBER" \ + --request-changes \ + --body "⚠️ This PR contains VULN tickets that do not match the target LTS product. Please review the JIRA ticket assignments and ensure they match the merge target branch." \ + --repo "$REPOSITORY" + + echo "✅ Requested changes on PR #$PR_NUMBER" + + - name: Fail workflow if JIRA errors found + if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outcome == 'failure' + run: | + echo "❌ JIRA PR check failed - errors were found in one or more commits" + exit 1 + + - name: Post final summary to PR + if: always() # Run even if previous steps failed + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} + REPOSITORY: ${{ steps.pr_metadata.outputs.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + CONCLUSION: ${{ job.status }} + run: | + if [ "$CONCLUSION" = "success" ]; then + EMOJI="✅" + STATUS="completed successfully" + else + EMOJI="❌" + STATUS="completed with issues" + fi + + BODY="$EMOJI **Validation checks $STATUS**\ + \ + View full results: $RUN_URL" + + gh pr comment "$PR_NUMBER" \ + --body "$BODY" \ + --repo "$REPOSITORY" || echo "⚠️ Failed to post summary comment" From c228ec8ebd6cea530cf1c8efa640a1bf32a6a2ff Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Mon, 22 Dec 2025 17:00:38 -0500 Subject: [PATCH 2/5] github actions: Remove is_fork changes This is not what we needed and needed removed. --- ...validate-kernel-commits-comment-secure.yml | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/validate-kernel-commits-comment-secure.yml b/.github/workflows/validate-kernel-commits-comment-secure.yml index 98ed41839d6d..10c10049a47d 100644 --- a/.github/workflows/validate-kernel-commits-comment-secure.yml +++ b/.github/workflows/validate-kernel-commits-comment-secure.yml @@ -123,17 +123,6 @@ jobs: echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT - - name: Check if PR is from fork - id: is_fork - run: | - if [ "${{ steps.pr_metadata.outputs.head_repo }}" = "${{ steps.pr_metadata.outputs.repository }}" ]; then - echo "result=false" >> $GITHUB_OUTPUT - echo "✅ Internal PR detected" - else - echo "result=false" >> $GITHUB_OUTPUT - echo "✅ Fork PR detected - JIRA checks will be skipped" - fi - - name: Post workflow run link to PR env: GH_TOKEN: ${{ github.token }} @@ -225,7 +214,6 @@ jobs: echo "✅ Posted interdiff comment to PR #$PR_NUMBER" - name: Checkout PR head for JIRA check - if: steps.is_fork.outputs.result == 'false' env: CLONE_URL: ${{ github.event_name == 'workflow_dispatch' && format('{0}/{1}.git', github.server_url, github.repository) || github.event.workflow_run.repository.clone_url }} BASE_REF: ${{ steps.pr_metadata.outputs.base_ref }} @@ -253,7 +241,6 @@ jobs: echo "✅ Checked out commit $HEAD_SHA to kernel-src-tree/ (on branch temp-pr-check)" - name: Checkout kernel-src-tree-tools for JIRA check - if: steps.is_fork.outputs.result == 'false' uses: actions/checkout@v4 with: repository: ctrliq/kernel-src-tree-tools @@ -261,26 +248,22 @@ jobs: path: kernel-src-tree-tools - name: Set up Python for JIRA check - if: steps.is_fork.outputs.result == 'false' uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install JIRA PR Check dependencies - if: steps.is_fork.outputs.result == 'false' run: | python -m pip install --upgrade pip pip install jira - name: Mask JIRA credentials - if: steps.is_fork.outputs.result == 'false' run: | echo "::add-mask::${{ secrets.JIRA_API_TOKEN }}" echo "::add-mask::${{ secrets.JIRA_API_USER }}" echo "::add-mask::${{ secrets.JIRA_URL }}" - name: Run JIRA PR Check - if: steps.is_fork.outputs.result == 'false' id: jira_check continue-on-error: true # Allow PR comments to be posted before failing workflow env: @@ -341,7 +324,7 @@ jobs: exit $EXIT_CODE - name: Comment PR with JIRA issues - if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_issues == 'true' + if: steps.jira_check.outputs.has_issues == 'true' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} @@ -366,7 +349,7 @@ jobs: echo "✅ Posted JIRA comment to PR #$PR_NUMBER" - name: Dismiss previous bot reviews if checks pass - if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outcome == 'success' && steps.jira_check.outputs.has_issues == 'false' + if: steps.jira_check.outcome == 'success' && steps.jira_check.outputs.has_issues == 'false' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} @@ -390,7 +373,7 @@ jobs: fi - name: Request changes if LTS mismatch - if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outputs.has_lts_mismatch == 'true' + if: steps.jira_check.outputs.has_lts_mismatch == 'true' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ steps.pr_metadata.outputs.pr_number }} @@ -404,7 +387,7 @@ jobs: echo "✅ Requested changes on PR #$PR_NUMBER" - name: Fail workflow if JIRA errors found - if: steps.is_fork.outputs.result == 'false' && steps.jira_check.outcome == 'failure' + if: steps.jira_check.outcome == 'failure' run: | echo "❌ JIRA PR check failed - errors were found in one or more commits" exit 1 From 098cda69e03b422a25d2bb581fa6c07ef98cb488 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Mon, 22 Dec 2025 17:48:04 -0500 Subject: [PATCH 3/5] github actions: address PR feedback Cleaning up some minor changes and makding checks a little safer. --- .../validate-kernel-commits-comment-secure.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate-kernel-commits-comment-secure.yml b/.github/workflows/validate-kernel-commits-comment-secure.yml index 10c10049a47d..7ee9e35bf6a5 100644 --- a/.github/workflows/validate-kernel-commits-comment-secure.yml +++ b/.github/workflows/validate-kernel-commits-comment-secure.yml @@ -144,7 +144,7 @@ jobs: run: | # Validate result files exist and are reasonable size if [ -f ckc_result.txt ]; then - FILE_SIZE=$(stat -f%z ckc_result.txt 2>/dev/null || stat -c%s ckc_result.txt 2>/dev/null) + FILE_SIZE=$(stat -c%s ckc_result.txt 2>/dev/null) if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit echo "❌ Security: ckc_result.txt file too large: $FILE_SIZE bytes" exit 1 @@ -153,7 +153,7 @@ jobs: fi if [ -f interdiff_result.txt ]; then - FILE_SIZE=$(stat -f%z interdiff_result.txt 2>/dev/null || stat -c%s interdiff_result.txt 2>/dev/null) + FILE_SIZE=$(stat -c%s interdiff_result.txt 2>/dev/null) if [ "$FILE_SIZE" -gt 1048576 ]; then # 1MB limit echo "❌ Security: interdiff_result.txt file too large: $FILE_SIZE bytes" exit 1 @@ -287,11 +287,13 @@ jobs: # Filter out any potential credential leaks from output # Use fixed string matching to avoid regex issues with special chars in tokens - FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v -F -e "jira-user" -e "jira-key" -e "basic_auth" -e "Authorization" -e "Bearer") + FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v -F -e "jira-user" -e "jira-key" -e "basic_auth" -e "Authorization" -e "Bearer" || true) # Additional safety: remove any lines that might contain the actual token - # Do this without putting the token in the command line - FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v "$JIRA_API_USER" || true) + # Do this without putting the token in the command linei + if [ -n "${JIRA_API_USER:-}" ] ; then + FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v "$JIRA_API_USER" || true) + fi echo "$FILTERED_OUTPUT" From f9a1f5b82fa50efb7440648ae7e881e494bff200 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Tue, 23 Dec 2025 14:56:22 -0500 Subject: [PATCH 4/5] github actions: Move secured split to old version. This is to replace the original with the split apart half of it so that there is no need to replace the content later or have several changes to the leaf branches. --- .../validate-kernel-commits-check-secure.yml | 281 ------------------ ...ml => validate-kernel-commits-comment.yml} | 2 +- .github/workflows/validate-kernel-commits.yml | 268 +++++++++-------- 3 files changed, 137 insertions(+), 414 deletions(-) delete mode 100644 .github/workflows/validate-kernel-commits-check-secure.yml rename .github/workflows/{validate-kernel-commits-comment-secure.yml => validate-kernel-commits-comment.yml} (99%) diff --git a/.github/workflows/validate-kernel-commits-check-secure.yml b/.github/workflows/validate-kernel-commits-check-secure.yml deleted file mode 100644 index 28e725b71b2a..000000000000 --- a/.github/workflows/validate-kernel-commits-check-secure.yml +++ /dev/null @@ -1,281 +0,0 @@ -name: Validate Kernel Commits - Check (Secure) - -on: - workflow_call: - # No inputs needed - uses github context from caller - -permissions: - contents: read - # No pull-requests: write needed - we don't comment here - -jobs: - validate-kernel-commits-check: - runs-on: ubuntu-latest - timeout-minutes: 120 - - steps: - - name: Validate and sanitize inputs - id: validate_inputs - env: - BASE_REF: ${{ github.base_ref }} - HEAD_REF: ${{ github.head_ref }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_COMMITS: ${{ github.event.pull_request.commits }} - run: | - # Validate base branch name (alphanumeric, dots, slashes, dashes, underscores, curly braces) - # Note: hyphen must be at end of character class or escaped to be literal - if ! [[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then - echo "❌ Invalid base branch name: $BASE_REF" - exit 1 - fi - - # Validate head branch name - if ! [[ "$HEAD_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then - echo "❌ Invalid head branch name: $HEAD_REF" - exit 1 - fi - - # Validate length (prevent resource exhaustion) - if [ ${#BASE_REF} -gt 255 ]; then - echo "❌ Base branch name too long" - exit 1 - fi - - if [ ${#HEAD_REF} -gt 255 ]; then - echo "❌ Head branch name too long" - exit 1 - fi - - # Validate PR number is numeric - if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then - echo "❌ Invalid PR number: $PR_NUMBER" - exit 1 - fi - - # Validate commits count is numeric - if ! [[ "$PR_COMMITS" =~ ^[0-9]+$ ]]; then - echo "❌ Invalid commits count: $PR_COMMITS" - exit 1 - fi - - # Pass validated values to environment - echo "BASE_REF=$BASE_REF" >> "$GITHUB_ENV" - echo "HEAD_REF=$HEAD_REF" >> "$GITHUB_ENV" - echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" - echo "PR_COMMITS=$PR_COMMITS" >> "$GITHUB_ENV" - - - name: Clone base branch - env: - BASE_CLONE_URL: ${{ github.event.pull_request.base.repo.clone_url }} - run: | - # Use environment variables to prevent injection - git clone --depth=1 --no-checkout "$BASE_CLONE_URL" -b "$BASE_REF" . - - - name: Fetch PR branch - env: - HEAD_CLONE_URL: ${{ github.event.pull_request.head.repo.clone_url }} - run: | - # Use environment variables to prevent command injection - git fetch --depth=$((PR_COMMITS + 1)) "$HEAD_CLONE_URL" "$HEAD_REF" - HEAD_SHA=$(git rev-parse FETCH_HEAD) - - # Validate SHA format (40 hex characters) - if ! [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then - echo "❌ Invalid SHA format: $HEAD_SHA" - exit 1 - fi - - echo "HEAD_SHA=$HEAD_SHA" >> "$GITHUB_ENV" - - - name: Verify PR branch isn't on stale base - run: | - if ! git merge-base --is-ancestor "$BASE_REF" "$HEAD_SHA"; then - echo "❌ PR branch must be rebased onto latest base branch commit" - exit 1 - fi - - - name: Fetch upstream mainline branch - run: | - # Determine the kernel version tag, since it is the most recent common - # ancestor between the base branch and the upstream mainline branch - MAKEFILE=$(git show "$BASE_REF":Makefile) - VERSION=$(grep -m1 -Po '(?<=^VERSION = )\d+' <<< "$MAKEFILE") - PATCHLEVEL=$(grep -m1 -Po '(?<=^PATCHLEVEL = )\d+' <<< "$MAKEFILE") - - # Validate VERSION and PATCHLEVEL are numeric - if ! [[ "$VERSION" =~ ^[0-9]+$ ]] || ! [[ "$PATCHLEVEL" =~ ^[0-9]+$ ]]; then - echo "❌ Invalid kernel version: $VERSION.$PATCHLEVEL" - exit 1 - fi - - # Fetch upstream mainline branch without tags, fetching only history - # that is newer than this kernel version tag. This reduces the amount - # of commits cloned knowing that there's no need to check any commits - # for Fixes commits older than what the base branch already contains - git fetch --no-tags --shallow-exclude="v$VERSION.$PATCHLEVEL" origin kernel-mainline - MAINLINE_SHA=$(git rev-parse FETCH_HEAD) - - # Validate SHA format - if ! [[ "$MAINLINE_SHA" =~ ^[0-9a-f]{40}$ ]]; then - echo "❌ Invalid mainline SHA format: $MAINLINE_SHA" - exit 1 - fi - - echo "MAINLINE_SHA=$MAINLINE_SHA" >> "$GITHUB_ENV" - - - name: Checkout kernel-src-tree-tools - uses: actions/checkout@v4 - with: - repository: ctrliq/kernel-src-tree-tools - ref: 'mainline' - path: kernel-src-tree-tools - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Run upstream fixes check - id: check-kernel-commits - working-directory: kernel-src-tree-tools - run: | - set +e # Don't exit on error, we want to capture the output - set -o pipefail # Capture exit code from python script, not tee - python3 check_kernel_commits.py \ - --repo .. \ - --pr_branch "$HEAD_SHA" \ - --base_branch "$BASE_REF" \ - --markdown \ - --upstream-ref "$MAINLINE_SHA" \ - --check-cves | tee ../ckc_result.txt - EXIT_CODE=$? - - # Check if the script failed - if [ $EXIT_CODE -ne 0 ]; then - echo "❌ Kernel commits check failed with exit code $EXIT_CODE" - exit $EXIT_CODE - fi - - # Check for findings: - # 1. Verify the success message exists - # 2. If it exists, check if there are any OTHER lines (which would indicate issues) - # 3. If success message doesn't exist, that's also a finding - if grep -q "All referenced commits exist upstream and have no Fixes: tags." ../ckc_result.txt; then - # Success message found, check if there are any other lines - LINE_COUNT=$(wc -l < ../ckc_result.txt) - if [ "$LINE_COUNT" -gt 1 ]; then - echo "has_findings=true" >> $GITHUB_OUTPUT - else - echo "has_findings=false" >> $GITHUB_OUTPUT - fi - else - # Success message not found, there must be findings - echo "has_findings=true" >> $GITHUB_OUTPUT - fi - - set -e # Re-enable exit on error - - - name: Install build dependencies for patchutils - run: | - sudo apt-get update - sudo apt-get install -y build-essential autoconf automake libtool gnulib - - - name: Clone and build custom patchutils - run: | - # Security: Pin to specific commit to prevent supply chain attacks - EXPECTED_COMMIT="60a60b3909d0e29c0ff286f6a73de4168977b097" - - # Clone repository - git clone https://github.com/kerneltoast/patchutils.git - cd patchutils - - # Fetch the specific commit we want - git fetch origin "$EXPECTED_COMMIT" - git checkout "$EXPECTED_COMMIT" - - # Verify we're on the expected commit - ACTUAL_COMMIT=$(git rev-parse HEAD) - if [ "$ACTUAL_COMMIT" != "$EXPECTED_COMMIT" ]; then - echo "❌ Security: Commit mismatch!" - echo "Expected: $EXPECTED_COMMIT" - echo "Actual: $ACTUAL_COMMIT" - exit 1 - fi - - # Build patchutils - ./bootstrap - ./configure - make -j$(nproc) - - # Verify the binary was created - if [ ! -x src/interdiff ]; then - echo "❌ Failed to build interdiff binary" - exit 1 - fi - - - name: Run interdiff check - id: interdiff - working-directory: kernel-src-tree-tools - run: | - set +e # Don't exit on error, we want to capture the output - set -o pipefail # Capture exit code from python script, not tee - python3 run_interdiff.py \ - --repo .. \ - --pr_branch "$HEAD_SHA" \ - --base_branch "$BASE_REF" \ - --markdown \ - --interdiff ../patchutils/src/interdiff | tee ../interdiff_result.txt - EXIT_CODE=$? - - # Check if the script failed - if [ $EXIT_CODE -ne 0 ]; then - echo "❌ Interdiff check failed with exit code $EXIT_CODE" - exit $EXIT_CODE - fi - - # Check for differences: - # 1. Verify the success message exists - # 2. If it exists, check if there are any OTHER lines (which would indicate differences) - # 3. If success message doesn't exist, that's also a difference - if grep -q "All backported commits match their upstream counterparts." ../interdiff_result.txt; then - # Success message found, check if there are any other lines - LINE_COUNT=$(wc -l < ../interdiff_result.txt) - if [ "$LINE_COUNT" -gt 1 ]; then - echo "has_differences=true" >> $GITHUB_OUTPUT - else - echo "has_differences=false" >> $GITHUB_OUTPUT - fi - else - # Success message not found, there must be differences - echo "has_differences=true" >> $GITHUB_OUTPUT - fi - - set -e # Re-enable exit on error - - - name: Save PR metadata for comment workflow - env: - HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} - REPOSITORY: ${{ github.repository }} - run: | - mkdir -p pr_metadata - - # Save validated metadata - echo "$PR_NUMBER" > pr_metadata/pr_number.txt - echo "$REPOSITORY" > pr_metadata/repository.txt - echo "$BASE_REF" > pr_metadata/base_ref.txt - echo "$HEAD_SHA" > pr_metadata/head_sha.txt - echo "$HEAD_REPO_FULL_NAME" > pr_metadata/head_repo.txt - - # Create a checksum of metadata for integrity verification - (cd pr_metadata && sha256sum *.txt > checksums.txt) - - - name: Upload check results - uses: actions/upload-artifact@v4 - if: always() # Upload even if checks fail - with: - name: check-results - path: | - ckc_result.txt - interdiff_result.txt - pr_metadata/ - retention-days: 3 # Increased from 1 to prevent premature deletion diff --git a/.github/workflows/validate-kernel-commits-comment-secure.yml b/.github/workflows/validate-kernel-commits-comment.yml similarity index 99% rename from .github/workflows/validate-kernel-commits-comment-secure.yml rename to .github/workflows/validate-kernel-commits-comment.yml index 7ee9e35bf6a5..3f4e78a09ed6 100644 --- a/.github/workflows/validate-kernel-commits-comment-secure.yml +++ b/.github/workflows/validate-kernel-commits-comment.yml @@ -1,4 +1,4 @@ -name: Validate Kernel Commits - Post Comments (Secure) +name: Validate Kernel Commits - Post Comments on: workflow_run: diff --git a/.github/workflows/validate-kernel-commits.yml b/.github/workflows/validate-kernel-commits.yml index 46b8a9d98bd3..f86c313752a9 100644 --- a/.github/workflows/validate-kernel-commits.yml +++ b/.github/workflows/validate-kernel-commits.yml @@ -6,29 +6,90 @@ on: permissions: contents: read - pull-requests: write + # No pull-requests: write needed - we don't comment here jobs: - validate-kernel-commits: + validate-kernel-commits-check: runs-on: ubuntu-latest timeout-minutes: 120 steps: + - name: Validate and sanitize inputs + id: validate_inputs + env: + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_COMMITS: ${{ github.event.pull_request.commits }} + run: | + # Validate base branch name (alphanumeric, dots, slashes, dashes, underscores, curly braces) + # Note: hyphen must be at end of character class or escaped to be literal + if ! [[ "$BASE_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then + echo "❌ Invalid base branch name: $BASE_REF" + exit 1 + fi + + # Validate head branch name + if ! [[ "$HEAD_REF" =~ ^[a-zA-Z0-9/_.{}-]+$ ]]; then + echo "❌ Invalid head branch name: $HEAD_REF" + exit 1 + fi + + # Validate length (prevent resource exhaustion) + if [ ${#BASE_REF} -gt 255 ]; then + echo "❌ Base branch name too long" + exit 1 + fi + + if [ ${#HEAD_REF} -gt 255 ]; then + echo "❌ Head branch name too long" + exit 1 + fi + + # Validate PR number is numeric + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number: $PR_NUMBER" + exit 1 + fi + + # Validate commits count is numeric + if ! [[ "$PR_COMMITS" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid commits count: $PR_COMMITS" + exit 1 + fi + + # Pass validated values to environment + echo "BASE_REF=$BASE_REF" >> "$GITHUB_ENV" + echo "HEAD_REF=$HEAD_REF" >> "$GITHUB_ENV" + echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" + echo "PR_COMMITS=$PR_COMMITS" >> "$GITHUB_ENV" + - name: Clone base branch + env: + BASE_CLONE_URL: ${{ github.event.pull_request.base.repo.clone_url }} run: | - # Optimization: don't checkout at the start because it's superfluous - git clone --depth=1 --no-checkout "${{ github.event.pull_request.base.repo.clone_url }}" -b "${{ github.base_ref }}" . + # Use environment variables to prevent injection + git clone --depth=1 --no-checkout "$BASE_CLONE_URL" -b "$BASE_REF" . - name: Fetch PR branch + env: + HEAD_CLONE_URL: ${{ github.event.pull_request.head.repo.clone_url }} run: | - # Fetch all of the commits in the PR plus one more commit before, - # which should be the head of the base branch - git fetch --depth=$((${{ github.event.pull_request.commits }} + 1)) "${{ github.event.pull_request.head.repo.clone_url }}" "${{ github.head_ref }}" - echo "HEAD_SHA=$(git rev-parse FETCH_HEAD)" >> "$GITHUB_ENV" + # Use environment variables to prevent command injection + git fetch --depth=$((PR_COMMITS + 1)) "$HEAD_CLONE_URL" "$HEAD_REF" + HEAD_SHA=$(git rev-parse FETCH_HEAD) + + # Validate SHA format (40 hex characters) + if ! [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "❌ Invalid SHA format: $HEAD_SHA" + exit 1 + fi + + echo "HEAD_SHA=$HEAD_SHA" >> "$GITHUB_ENV" - name: Verify PR branch isn't on stale base run: | - if ! git merge-base --is-ancestor "${{ github.base_ref }}" "$HEAD_SHA"; then + if ! git merge-base --is-ancestor "$BASE_REF" "$HEAD_SHA"; then echo "❌ PR branch must be rebased onto latest base branch commit" exit 1 fi @@ -37,16 +98,30 @@ jobs: run: | # Determine the kernel version tag, since it is the most recent common # ancestor between the base branch and the upstream mainline branch - MAKEFILE=$(git show "${{ github.base_ref }}":Makefile) + MAKEFILE=$(git show "$BASE_REF":Makefile) VERSION=$(grep -m1 -Po '(?<=^VERSION = )\d+' <<< "$MAKEFILE") PATCHLEVEL=$(grep -m1 -Po '(?<=^PATCHLEVEL = )\d+' <<< "$MAKEFILE") + # Validate VERSION and PATCHLEVEL are numeric + if ! [[ "$VERSION" =~ ^[0-9]+$ ]] || ! [[ "$PATCHLEVEL" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid kernel version: $VERSION.$PATCHLEVEL" + exit 1 + fi + # Fetch upstream mainline branch without tags, fetching only history # that is newer than this kernel version tag. This reduces the amount # of commits cloned knowing that there's no need to check any commits # for Fixes commits older than what the base branch already contains git fetch --no-tags --shallow-exclude="v$VERSION.$PATCHLEVEL" origin kernel-mainline - echo "MAINLINE_SHA=$(git rev-parse FETCH_HEAD)" >> "$GITHUB_ENV" + MAINLINE_SHA=$(git rev-parse FETCH_HEAD) + + # Validate SHA format + if ! [[ "$MAINLINE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "❌ Invalid mainline SHA format: $MAINLINE_SHA" + exit 1 + fi + + echo "MAINLINE_SHA=$MAINLINE_SHA" >> "$GITHUB_ENV" - name: Checkout kernel-src-tree-tools uses: actions/checkout@v4 @@ -69,7 +144,7 @@ jobs: python3 check_kernel_commits.py \ --repo .. \ --pr_branch "$HEAD_SHA" \ - --base_branch "${{ github.base_ref }}" \ + --base_branch "$BASE_REF" \ --markdown \ --upstream-ref "$MAINLINE_SHA" \ --check-cves | tee ../ckc_result.txt @@ -100,18 +175,6 @@ jobs: set -e # Re-enable exit on error - - name: Comment on PR if issues found - if: steps.check-kernel-commits.outputs.has_findings == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - if ! gh pr comment ${{ github.event.pull_request.number }} \ - --body-file ckc_result.txt \ - --repo "${{ github.repository }}"; then - echo "❌ Failed to post check-kernel-commits comment to PR" - exit 1 - fi - - name: Install build dependencies for patchutils run: | sudo apt-get update @@ -119,12 +182,37 @@ jobs: - name: Clone and build custom patchutils run: | - git clone https://github.com/kerneltoast/patchutils.git --depth=1 --revision=60a60b3909d0e29c0ff286f6a73de4168977b097 + # Security: Pin to specific commit to prevent supply chain attacks + EXPECTED_COMMIT="60a60b3909d0e29c0ff286f6a73de4168977b097" + + # Clone repository + git clone https://github.com/kerneltoast/patchutils.git cd patchutils + + # Fetch the specific commit we want + git fetch origin "$EXPECTED_COMMIT" + git checkout "$EXPECTED_COMMIT" + + # Verify we're on the expected commit + ACTUAL_COMMIT=$(git rev-parse HEAD) + if [ "$ACTUAL_COMMIT" != "$EXPECTED_COMMIT" ]; then + echo "❌ Security: Commit mismatch!" + echo "Expected: $EXPECTED_COMMIT" + echo "Actual: $ACTUAL_COMMIT" + exit 1 + fi + + # Build patchutils ./bootstrap ./configure make -j$(nproc) + # Verify the binary was created + if [ ! -x src/interdiff ]; then + echo "❌ Failed to build interdiff binary" + exit 1 + fi + - name: Run interdiff check id: interdiff working-directory: kernel-src-tree-tools @@ -134,7 +222,7 @@ jobs: python3 run_interdiff.py \ --repo .. \ --pr_branch "$HEAD_SHA" \ - --base_branch "${{ github.base_ref }}" \ + --base_branch "$BASE_REF" \ --markdown \ --interdiff ../patchutils/src/interdiff | tee ../interdiff_result.txt EXIT_CODE=$? @@ -164,114 +252,30 @@ jobs: set -e # Re-enable exit on error - - name: Comment on PR if interdiff differences found - if: steps.interdiff.outputs.has_differences == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - if ! gh pr comment ${{ github.event.pull_request.number }} \ - --body-file interdiff_result.txt \ - --repo "${{ github.repository }}"; then - echo "❌ Failed to post interdiff comment to PR" - exit 1 - fi - - - name: Determine if JIRA PR check should run - id: should_check_jira - run: | - if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then - echo "result=true" >> $GITHUB_OUTPUT - else - echo "result=false" >> $GITHUB_OUTPUT - fi - - - name: Install JIRA PR Check dependencies - if: steps.should_check_jira.outputs.result == 'true' - run: | - python -m pip install --upgrade pip - pip install jira - - - name: Mask JIRA credentials - if: steps.should_check_jira.outputs.result == 'true' - run: | - echo "::add-mask::${{ secrets.JIRA_API_TOKEN }}" - echo "::add-mask::${{ secrets.JIRA_API_USER }}" - echo "::add-mask::${{ secrets.JIRA_URL }}" - - - name: Run JIRA PR Check - if: steps.should_check_jira.outputs.result == 'true' - id: jira_check - continue-on-error: true # Allow PR comments to be posted before failing workflow + - name: Save PR metadata for comment workflow env: - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - JIRA_API_USER: ${{ secrets.JIRA_API_USER }} - JIRA_URL: ${{ secrets.JIRA_URL }} - working-directory: kernel-src-tree-tools + HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} + REPOSITORY: ${{ github.repository }} run: | - # Run script and capture output, ensuring credentials are never echoed - set +x # Disable command echo to prevent credential exposure - set +e # Don't exit on error, we want to capture the output - OUTPUT=$(python3 jira_pr_check.py \ - --kernel-src-tree .. \ - --merge-target "${{ github.base_ref }}" \ - --pr-branch "$HEAD_SHA" 2>&1) - EXIT_CODE=$? - - # Filter out any potential credential leaks from output - FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v "jira-user\|jira-key\|basic_auth\|Authorization\|$JIRA_API_TOKEN") - - echo "$FILTERED_OUTPUT" - { - echo "output<> $GITHUB_OUTPUT - - # Check if there are any issues based on output patterns - if echo "$FILTERED_OUTPUT" | grep -q "❌ Errors:"; then - echo "has_issues=true" >> $GITHUB_OUTPUT - - # Check specifically for LTS mismatch errors - if echo "$FILTERED_OUTPUT" | grep -q "expects branch"; then - echo "has_lts_mismatch=true" >> $GITHUB_OUTPUT - else - echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT - fi - elif echo "$FILTERED_OUTPUT" | grep -q "⚠️ Warnings:"; then - echo "has_issues=true" >> $GITHUB_OUTPUT - echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT - else - echo "has_issues=false" >> $GITHUB_OUTPUT - echo "has_lts_mismatch=false" >> $GITHUB_OUTPUT - fi - - # Exit with the script's exit code - exit $EXIT_CODE + mkdir -p pr_metadata - - name: Comment PR with JIRA issues - if: steps.should_check_jira.outputs.result == 'true' && steps.jira_check.outputs.has_issues == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - if ! gh pr comment ${{ github.event.pull_request.number }} \ - --body "${{ steps.jira_check.outputs.output }}" \ - --repo "${{ github.repository }}"; then - echo "❌ Failed to post JIRA check comment to PR" - exit 1 - fi + # Save validated metadata + echo "$PR_NUMBER" > pr_metadata/pr_number.txt + echo "$REPOSITORY" > pr_metadata/repository.txt + echo "$BASE_REF" > pr_metadata/base_ref.txt + echo "$HEAD_SHA" > pr_metadata/head_sha.txt + echo "$HEAD_REPO_FULL_NAME" > pr_metadata/head_repo.txt - - name: Request changes if LTS mismatch - if: steps.should_check_jira.outputs.result == 'true' && steps.jira_check.outputs.has_lts_mismatch == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - gh pr review ${{ github.event.pull_request.number }} \ - --request-changes \ - --body "⚠️ This PR contains VULN tickets that do not match the target LTS product. Please review the JIRA ticket assignments and ensure they match the merge target branch." \ - --repo "${{ github.repository }}" + # Create a checksum of metadata for integrity verification + (cd pr_metadata && sha256sum *.txt > checksums.txt) - - name: Fail workflow if JIRA errors found - if: steps.should_check_jira.outputs.result == 'true' && steps.jira_check.outcome == 'failure' - run: | - echo "❌ JIRA PR check failed - errors were found in one or more commits" - exit 1 + - name: Upload check results + uses: actions/upload-artifact@v4 + if: always() # Upload even if checks fail + with: + name: check-results + path: | + ckc_result.txt + interdiff_result.txt + pr_metadata/ + retention-days: 3 # Increased from 1 to prevent premature deletion From 75f24a6fcd7adff51634c8049de436b85b503045 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Tue, 23 Dec 2025 15:57:08 -0500 Subject: [PATCH 5/5] github actions: Address PR CoPilot Feedback --- .../validate-kernel-commits-comment.yml | 16 ++++++---------- .github/workflows/validate-kernel-commits.yml | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/validate-kernel-commits-comment.yml b/.github/workflows/validate-kernel-commits-comment.yml index 3f4e78a09ed6..782ec6b02c12 100644 --- a/.github/workflows/validate-kernel-commits-comment.yml +++ b/.github/workflows/validate-kernel-commits-comment.yml @@ -78,7 +78,7 @@ jobs: fi # Validate repository format (owner/repo) - if ! [[ "$REPOSITORY" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then + if ! [[ "$REPOSITORY" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then echo "❌ Security: Invalid repository format: $REPOSITORY" exit 1 fi @@ -108,7 +108,7 @@ jobs: fi # Validate head repo format (can be empty for deleted forks) - if [ -n "$HEAD_REPO" ] && ! [[ "$HEAD_REPO" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then + if [ -n "$HEAD_REPO" ] && ! [[ "$HEAD_REPO" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then echo "❌ Security: Invalid head repo format: $HEAD_REPO" exit 1 fi @@ -132,9 +132,7 @@ jobs: run: | BODY="🤖 **Validation Checks In Progress** \ \ - Workflow run: $RUN_URL \ - \ - This comment will be updated with check results when complete." + Workflow run: $RUN_URL " gh pr comment "$PR_NUMBER" \ --body "$BODY" \ @@ -290,9 +288,9 @@ jobs: FILTERED_OUTPUT=$(echo "$OUTPUT" | grep -v -F -e "jira-user" -e "jira-key" -e "basic_auth" -e "Authorization" -e "Bearer" || true) # Additional safety: remove any lines that might contain the actual token - # Do this without putting the token in the command linei + # Do this without putting the token in the command line if [ -n "${JIRA_API_USER:-}" ] ; then - FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v "$JIRA_API_USER" || true) + FILTERED_OUTPUT=$(echo "$FILTERED_OUTPUT" | grep -v -F "$JIRA_API_USER" || true) fi echo "$FILTERED_OUTPUT" @@ -411,9 +409,7 @@ jobs: STATUS="completed with issues" fi - BODY="$EMOJI **Validation checks $STATUS**\ - \ - View full results: $RUN_URL" + BODY="$EMOJI **Validation checks $STATUS** View full results: $RUN_URL" gh pr comment "$PR_NUMBER" \ --body "$BODY" \ diff --git a/.github/workflows/validate-kernel-commits.yml b/.github/workflows/validate-kernel-commits.yml index f86c313752a9..bf27b7f22d7f 100644 --- a/.github/workflows/validate-kernel-commits.yml +++ b/.github/workflows/validate-kernel-commits.yml @@ -278,4 +278,4 @@ jobs: ckc_result.txt interdiff_result.txt pr_metadata/ - retention-days: 3 # Increased from 1 to prevent premature deletion + retention-days: 3 # Increased from 1 (then 3) to prevent premature deletion and support manual follow-ups