diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 000000000..6133000b7 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,12 @@ +#!/bin/bash + +# Pre-commit hook that runs the suspicious patterns check on staged files + +# Get the repository's root directory +repo_root=$(git rev-parse --show-toplevel) + +# Run the suspicious patterns script in pre-commit mode +"$repo_root/suspicious_patterns.sh" --pre-commit + +# Exit with the same code as the script +exit $? diff --git a/.githooks/setup.sh b/.githooks/setup.sh new file mode 100644 index 000000000..eb77aa11c --- /dev/null +++ b/.githooks/setup.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Configuring git to use .githooks directory..." +git config core.hooksPath .githooks diff --git a/.github/workflows/checkpatterns.yml b/.github/workflows/checkpatterns.yml deleted file mode 100644 index 8b60a12e5..000000000 --- a/.github/workflows/checkpatterns.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: checkpatterns - -on: [push, pull_request] - -jobs: - checkpatterns: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for suspicious patterns - run: | - if [ -f "suspicious_patterns.sh" ]; then - bash suspicious_patterns.sh - else - echo "Warning: suspicious_patterns.sh not found, skipping check" - # Still exit with success for compatibility with dependent jobs - exit 0 - fi diff --git a/suspicious_patterns.sh b/suspicious_patterns.sh index 39287a6a1..421919d94 100755 --- a/suspicious_patterns.sh +++ b/suspicious_patterns.sh @@ -1,25 +1,123 @@ #!/bin/bash +# Exit on error, undefined variables, and pipe failures +set -euo pipefail + +# Enable debug mode if DEBUG environment variable is set +[[ "${DEBUG:-}" == "1" ]] && set -x + +# This script prevents accidental commits of XRPL/Ripple cryptographic keys +# It searches for: +# - Secret seeds (s...) - can derive keypairs +# - Private keys (p...) - validator private keys! +# - Raw key material (02/03/ED + hex) - compressed public keys or Ed25519 keys +# +# Usage: +# suspicious_patterns.sh # Check last commit (for CI) +# suspicious_patterns.sh --pre-commit # Check staged files (for pre-commit hook) +# +# WARNING: If this catches a real key in CI, that key is already compromised! +# The key has been pushed to the git history and must be immediately decommissioned. +# +# To mark keys as safe, add comment: // not-suspicious +# Files excluded: See exclude_files array below +# Lines excluded: Matching exclude_pattern regex below + +# Pattern for lines to exclude from checking +exclude_pattern="public_key|not-suspicious" + +# Array of files to exclude from checking (paths relative to repo root) +exclude_files=( + "src/test/app/Import_test.cpp" + "cfg/validators-example.txt" +) + # Get the repository's root directory repo_root=$(git rev-parse --show-toplevel) -# Get a list of files changed in the last commit with their relative paths -files_changed=$(git diff --name-only --relative HEAD~1 HEAD) +# Determine which files to check based on context: +# 1. Pre-commit hook: Check staged files before commit +# 2. GitHub PR: Check all files changed in the PR (HEAD is a synthetic merge commit) +# 3. Regular push/CI: Check files in the last real commit +if [[ "${1:-}" == "--pre-commit" ]]; then + # Pre-commit mode: Check what's about to be committed + files_changed=$(git diff --cached --name-only --relative) + mode="staged files" +else + # CI mode - need to handle two different scenarios + if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then + # GitHub PR event: HEAD is a synthetic merge commit created by GitHub + # that merges PR branch into base. Must diff against base to get only PR files. + base_ref="${GITHUB_BASE_REF:-dev}" + + # Ensure we have the base branch (GitHub Actions shallow clone might not have it) + if ! git rev-parse --verify "origin/$base_ref" >/dev/null 2>&1; then + echo "Fetching base branch origin/$base_ref..." + git fetch --depth=1 origin "$base_ref" + fi + + # Since there's no merge base in shallow clones, we need to be creative + # Save current HEAD, switch to base, then diff the trees + current_head=$(git rev-parse HEAD) + echo "Comparing against base branch origin/$base_ref..." + + # Get the tree objects to compare (this works even without shared history) + base_tree=$(git rev-parse "origin/$base_ref^{tree}") + head_tree=$(git rev-parse "$current_head^{tree}") + + # Compare the two trees directly + files_changed=$(git diff --name-only "$base_tree" "$head_tree") + mode="PR changes" + else + # Regular push event: Check the actual commit that was pushed + # Using 'git show' works even with shallow clones (no HEAD~1 needed) + # See: https://github.com/Xahau/xahaud/actions/runs/15492442104/job/43620965462#step:3:11 + files_changed=$(git show --name-only --pretty=format:'' HEAD) + mode="last commit" + fi +fi + +echo "Checking $mode for suspicious patterns..." + +# Show additional info in CI or when verbose mode is enabled +if [[ -n "${CI:-}" ]] || [[ "${VERBOSE:-}" == "1" ]]; then + if [[ "${1:-}" != "--pre-commit" ]]; then + echo "Commit: $(git rev-parse HEAD)" + fi + echo "Files to check:" + if [[ -n "$files_changed" ]]; then + echo "$files_changed" | nl + else + echo " (none)" + fi +fi # Loop through each file and search for the patterns for file in $files_changed; do - # Skip if the file is Import_test.cpp (exact filename match regardless of path) - if [[ "$(basename "$file")" == "Import_test.cpp" ]]; then - continue - fi + # Check if file should be excluded (exact path match) + for excluded in "${exclude_files[@]}"; do + if [[ "$file" == "$excluded" ]]; then + continue 2 # Continue outer loop + fi + done # Construct the absolute path absolute_path="$repo_root/$file" # Check if the file exists (it might have been deleted) if [ -f "$absolute_path" ]; then - # Search the file for the given patterns, but exclude lines containing 'public_key' - grep_output=$(grep -n -E '(([^rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]|^)(s|p)[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,60}([^(]|$)))|([^A-Fa-f0-9](02|03|ED)[A-Fa-f0-9]{64})' "$absolute_path" | grep -v "public_key") + # Get file content based on mode + if [[ "${1:-}" == "--pre-commit" ]]; then + # For staged files, use git show with the staging area + file_content=$(git show ":$file" 2>/dev/null) + else + # For committed files, read from disk + file_content=$(cat "$absolute_path") + fi + + # Search the file content for the given patterns, but exclude lines matching the exclusion pattern + # Use || true to prevent grep from failing the script when no matches are found + grep_output=$(echo "$file_content" | grep -n -E '(([^rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]|^)(s|p)[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,60}([^(]|$)))|([^A-Fa-f0-9](02|03|ED)[A-Fa-f0-9]{64})' | grep -vE "$exclude_pattern" || true) # Check if grep found any matches if [ ! -z "$grep_output" ]; then