From 47e0ae2d1d0d623c401aa9db690a1044eaae3515 Mon Sep 17 00:00:00 2001 From: AYOMI Date: Wed, 24 Jun 2026 17:43:41 +0000 Subject: [PATCH] Implemented the requested GitLeaks-based secret scanning support --- .github/workflows/secret-scan.yml | 109 ++++++++++++++++++++++++++++++ .gitleaks.baseline.toml | 10 +++ .gitleaks.toml | 36 ++++++++++ .husky/pre-commit | 1 + .pre-commit-config.yaml | 11 +++ docs/SECURITY.md | 55 +++++++++++++++ scripts/pre-commit-gitleaks.sh | 21 ++++++ scripts/revoke-stellar-key.sh | 90 ++++++++++++++++++++++++ 8 files changed, 333 insertions(+) create mode 100644 .github/workflows/secret-scan.yml create mode 100644 .gitleaks.baseline.toml create mode 100644 .gitleaks.toml create mode 100644 .pre-commit-config.yaml create mode 100644 docs/SECURITY.md create mode 100644 scripts/pre-commit-gitleaks.sh create mode 100644 scripts/revoke-stellar-key.sh diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 00000000..9f8155c4 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,109 @@ +name: Secret Scan + +on: + push: + branches: + - main + - 'release/**' + pull_request: + branches: + - main + - 'release/**' + +permissions: + contents: read + pull-requests: write + +jobs: + gitleaks-history-scan: + name: Full Git history secret scan + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run GitLeaks full history scan + id: gitleaks-scan + continue-on-error: true + uses: docker://zricethezav/gitleaks:latest + with: + args: detect --source . --config .gitleaks.toml --baseline .gitleaks.baseline.toml --report-format json --report-path gitleaks-report.json --verbose + + - name: Upload GitLeaks report + if: always() + uses: actions/upload-artifact@v4 + with: + name: gitleaks-report + path: gitleaks-report.json + + - name: Notify Slack on secret detection + if: steps.gitleaks-scan.outcome == 'failure' && env.SLACK_WEBHOOK_URL != '' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SECRET_SCAN_SLACK_WEBHOOK_URL }} + REPO: ${{ github.repository }} + REF: ${{ github.ref }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL is not set; skipping Slack notification." + exit 0 + fi + + payload=$(jq -n --arg text "GitLeaks detected potential secrets in \`$REPO\` on \`$REF\`. <${RUN_URL}|View the workflow run>." '{text: $text}') + curl -sS -X POST -H 'Content-Type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL" + + - name: Extract leaked Stellar secrets + if: steps.gitleaks-scan.outcome == 'failure' + id: stellar-leak + run: | + leak_count=$(jq '[.[] | select(.rule_id == "stellar-secret-key")] | length' gitleaks-report.json) + echo "leak_count=$leak_count" >> "$GITHUB_OUTPUT" + if [ "$leak_count" -gt 0 ]; then + leaked_key=$(jq -r '[.[] | select(.rule_id == "stellar-secret-key")][0].strings[0]' gitleaks-report.json) + echo "leaked_key=$leaked_key" >> "$GITHUB_OUTPUT" + fi + + - name: Revoke leaked Stellar key + if: steps.stellar-leak.outputs.leak_count != '' && steps.stellar-leak.outputs.leak_count != '0' + env: + STELLAR_HORIZON_URL: ${{ secrets.STELLAR_HORIZON_URL }} + STELLAR_ROTATION_SIGNER_SECRET: ${{ secrets.STELLAR_ROTATION_SIGNER_SECRET }} + run: | + npm ci --ignore-scripts + bash scripts/revoke-stellar-key.sh --leaked-key "${{ steps.stellar-leak.outputs.leaked_key }}" + + - name: Comment on PR with remediation steps + if: steps.gitleaks-scan.outcome == 'failure' && github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ⚠️ **Secret scan detected potential sensitive data in this PR.** + + Please remove the exposed secret from the branch and rewrite history before merging. + + Recommended remediation steps: + 1. Identify the leaked file(s) in the GitLeaks report. + 2. Remove the secret from the file and commit the fix. + 3. Rewrite history to remove the secret from earlier commits using `git filter-repo` or `git filter-branch`. + - Example with `git filter-repo`: + ```bash + git filter-repo --path --invert-paths + ``` + - Example with `git filter-branch`: + ```bash + git filter-branch --force --index-filter \ + 'git rm --cached --ignore-unmatch ' --prune-empty --tag-name-filter cat -- --all + ``` + 4. Force-push the cleaned branch and create a new PR. + + If this detection includes a Stellar secret key, the CI workflow will attempt automated key rotation if configured. + + - name: Fail build when secrets are detected + if: steps.gitleaks-scan.outcome == 'failure' + run: | + echo "GitLeaks detected secrets in repository history. Failing job." + exit 1 diff --git a/.gitleaks.baseline.toml b/.gitleaks.baseline.toml new file mode 100644 index 00000000..429fec66 --- /dev/null +++ b/.gitleaks.baseline.toml @@ -0,0 +1,10 @@ +title = "SubTrackr GitLeaks baseline" +description = "Allowlist for known false positives and approved non-secret patterns." + +# Add known false positives here to keep GitLeaks scans actionable. +# Example: +# [[allowlist]] +# description = "Known false positive sample data in documentation" +# regex = '''EXAMPLE_BASE64_PLACEHOLDER''' +# paths = ["docs/SECURITY.md"] + diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..db75ed53 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,36 @@ +title = "SubTrackr GitLeaks configuration" +description = "Detect Stellar secrets, API keys, JWTs, private keys, and high-entropy data." + +[[rules]] +id = "stellar-secret-key" +description = "Stellar secret key starting with S and 56 characters" +regex = '''S[A-Z2-7]{55}''' +tags = ["stellar", "secret"] + +[[rules]] +id = "stripe-secret-key" +description = "Stripe API secret key" +regex = '''sk_(live|test)_[0-9A-Za-z]{24,}''' +tags = ["api-key", "stripe"] + +[[rules]] +id = "jwt-token" +description = "JSON Web Token" +regex = '''eyJ[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}''' +tags = ["jwt", "token"] +entropy = 4.5 +entropyAlgorithm = "shannon" + +[[rules]] +id = "pem-private-key" +description = "PEM private key block" +regex = '''-----BEGIN (RSA|EC|OPENSSH|PRIVATE) PRIVATE KEY-----''' +tags = ["private-key", "pem"] + +[[rules]] +id = "high-entropy-encoded-data" +description = "High-entropy base64-like string or binary secret" +regex = '''[A-Za-z0-9+/]{25,}={0,2}''' +tags = ["entropy", "binary"] +entropy = 4.5 +entropyAlgorithm = "shannon" diff --git a/.husky/pre-commit b/.husky/pre-commit index 65cf4347..1a0bfde1 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ NODE_OPTIONS=--max-old-space-size=8192 npx lint-staged --concurrent false +bash scripts/pre-commit-gitleaks.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9316a92c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +minimum_pre_commit_version: '2.19.0' + +repos: + - repo: local + hooks: + - id: gitleaks-staged + name: GitLeaks staged secret scan + entry: bash scripts/pre-commit-gitleaks.sh + language: system + pass_filenames: false + stages: [commit] diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 00000000..40eae328 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,55 @@ +# Security and Secret Management + +This guide describes how SubTrackr protects sensitive credentials and how contributors should manage secrets. + +## Secret Scanning + +SubTrackr now enforces GitLeaks-based secret scanning in both local pre-commit hooks and CI. + +- `.gitleaks.toml`: GitLeaks configuration for Stellar secrets, API keys, JWTs, private keys, and high-entropy binary data. +- `.pre-commit-config.yaml`: Captures staged file scans before commit. +- `.github/workflows/secret-scan.yml`: Scans full git history on pushes to `main` and release branches. +- `.gitleaks.baseline.toml`: Known false positives are allowed via baseline exceptions. + +## Contributor Workflow + +1. Install repo hooks: + ```bash + npm install + npx husky install + npx pre-commit install + ``` +2. Before committing, staged files are scanned automatically. +3. If GitLeaks detects a secret, fix the offending file before committing. + +## What is scanned + +- Stellar secret keys beginning with `S` and 56-character encoded payload. +- Stripe-style API keys such as `sk_live_...` and `sk_test_...`. +- JWT tokens like `eyJ...`. +- PEM private key blocks (`BEGIN RSA PRIVATE KEY`, `BEGIN EC PRIVATE KEY`, etc.). +- High-entropy base64-like strings or binary content with entropy above 4.5 bits/byte. + +## CI Detection and Alerting + +The CI workflow uses GitLeaks Docker image to run a full repository scan. + +On detection: +- A Slack alert is sent to the configured security webhook. +- If a Stellar secret is found, the workflow attempts key rotation with `scripts/revoke-stellar-key.sh`. +- Pull requests receive an automated comment with remediation instructions. + +## Remediation + +If a secret is found in history: +1. Remove it from the file. +2. Rewrite git history to eliminate committed exposure. +3. Force-push the cleaned branch. +4. Rotate the exposed credentials in the affected system. + +## Best Practices + +- Never store secrets in source control. +- Use environment variables or secrets management tools. +- Rotate leaked keys immediately. +- Audit Git history regularly and keep `.gitleaks.baseline.toml` updated with only verified false positives. diff --git a/scripts/pre-commit-gitleaks.sh b/scripts/pre-commit-gitleaks.sh new file mode 100644 index 00000000..c47904f8 --- /dev/null +++ b/scripts/pre-commit-gitleaks.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +run_gitleaks() { + if command -v gitleaks >/dev/null 2>&1; then + gitleaks detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml + elif command -v npx >/dev/null 2>&1; then + npx gitleaks detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml + elif command -v docker >/dev/null 2>&1; then + docker run --rm -v "$REPO_ROOT":/repo -w /repo zricethezav/gitleaks:latest detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml + else + echo "Error: gitleaks CLI is not installed, and Docker is unavailable." + echo "Install gitleaks or Docker to enable the pre-commit secret scan." + return 1 + fi +} + +run_gitleaks diff --git a/scripts/revoke-stellar-key.sh b/scripts/revoke-stellar-key.sh new file mode 100644 index 00000000..8d53714d --- /dev/null +++ b/scripts/revoke-stellar-key.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat < + +Automatically rotate a leaked Stellar secret key by creating a new signer key pair. +Requires: + STELLAR_HORIZON_URL + STELLAR_ROTATION_SIGNER_SECRET + +EOF + exit 1 +} + +LEAKED_KEY="" +while [[ $# -gt 0 ]]; do + case "$1" in + --leaked-key) + LEAKED_KEY="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +if [[ -z "$LEAKED_KEY" ]]; then + echo "Error: --leaked-key is required" + usage +fi + +if [[ -z "${STELLAR_HORIZON_URL:-}" ]]; then + echo "Error: STELLAR_HORIZON_URL must be set" + exit 1 +fi + +if [[ -z "${STELLAR_ROTATION_SIGNER_SECRET:-}" ]]; then + echo "Error: STELLAR_ROTATION_SIGNER_SECRET must be set" + exit 1 +fi + +command -v python3 >/dev/null 2>&1 || { echo "python3 is required"; exit 1; } + +python3 <<'PY' +import os +import sys +from stellar_sdk import Keypair, Server, TransactionBuilder, Network + +leaked_key = os.environ['LEAKED_KEY'] +horizon_url = os.environ['STELLAR_HORIZON_URL'] +signer_secret = os.environ['STELLAR_ROTATION_SIGNER_SECRET'] + +if not leaked_key.startswith('S'): + print('Invalid Stellar secret key format', file=sys.stderr) + sys.exit(1) + +old_kp = Keypair.from_secret(leaked_key) +new_kp = Keypair.random() + +server = Server(horizon_url=horizon_url) +account = server.load_account(old_kp.public_key) +base_fee = server.fetch_base_fee() +network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE + +if 'testnet' in horizon_url: + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + +transaction = ( + TransactionBuilder( + source_account=account, + network_passphrase=network_passphrase, + base_fee=base_fee, + ) + .append_set_options_op(master_weight=0, low_threshold=1, med_threshold=1, high_threshold=1) + .append_set_options_op(signer={'ed25519_public_key': new_kp.public_key, 'weight': 1}) + .set_timeout(180) + .build() + +transaction.sign(old_kp) +transaction.sign(signer_secret) +response = server.submit_transaction(transaction) +print('Rotated Stellar secret key successfully:') +print('New public key:', new_kp.public_key) +print('New secret key:', new_kp.secret) +print('Transaction hash:', response['hash']) +PY