diff --git a/.github/workflows/secret-scan-pr.yml b/.github/workflows/secret-scan-pr.yml new file mode 100644 index 0000000..36e9462 --- /dev/null +++ b/.github/workflows/secret-scan-pr.yml @@ -0,0 +1,119 @@ +name: secret-scan-pr + +# Block PRs that introduce hardcoded secrets in their diff. +# +# Scope: scans only commits added by THIS PR (base..head), not the whole +# repo. The org-wide history scan is a separate scheduled job — this +# workflow is the per-PR gate. +# +# Requires .gitleaks.toml at repo root. If absent, gitleaks falls back +# to its built-in defaults (less Plaud-tuned, more noise). + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: secret-scan-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + gitleaks: + name: gitleaks (diff scan) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout with full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch base ref + run: git fetch --no-tags origin "${{ github.event.pull_request.base.ref }}" + + - name: Install gitleaks + env: + GITLEAKS_VERSION: "8.21.2" + run: | + set -euo pipefail + curl -fsSL \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + -o /tmp/gitleaks.tar.gz + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + sudo mv /tmp/gitleaks /usr/local/bin/gitleaks + gitleaks version + + - name: Scan PR diff for secrets + id: scan + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set +e + CONFIG_ARG="" + if [ -f .gitleaks.toml ]; then + CONFIG_ARG="--config=.gitleaks.toml" + else + echo "::warning::No .gitleaks.toml found; using built-in defaults." + fi + gitleaks git \ + $CONFIG_ARG \ + --log-opts="${BASE_SHA}..${HEAD_SHA}" \ + --report-format=json \ + --report-path=gitleaks-report.json \ + --redact=50 \ + --no-banner \ + --exit-code=1 + STATUS=$? + # Always succeed at this step; final pass/fail is decided after summary. + echo "status=$STATUS" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Render PR job summary + if: always() + run: | + set -euo pipefail + { + echo "## 🔐 Secret scan results" + echo + if [ ! -s gitleaks-report.json ] \ + || [ "$(jq 'length' gitleaks-report.json 2>/dev/null || echo 0)" = "0" ]; then + echo "✅ No new secrets detected in this PR's diff." + else + N=$(jq 'length' gitleaks-report.json) + echo "❌ **${N}** potential secret(s) detected. PR is blocked." + echo + echo "| Rule | File:Line | Commit | Author |" + echo "|------|-----------|--------|--------|" + jq -r '.[] | "| \(.RuleID) | `\(.File):\(.StartLine)` | `\(.Commit[0:10])` | \(.Author) |"' \ + gitleaks-report.json + echo + echo "**Next steps:**" + echo "1. Confirm whether each match is a real secret or a false positive." + echo "2. If **real** → revoke at the provider, rotate into the secrets manager, then force-push the cleaned commit. Do *not* simply delete the line in a new commit (the secret stays in git history)." + echo "3. If **false positive** → add the path or pattern to \`.gitleaks.toml\` in this PR with a comment explaining why it is safe." + echo + echo "_Secret values are redacted in this report. Full JSON in the \`gitleaks-report\` artifact._" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload report artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: gitleaks-report + path: gitleaks-report.json + if-no-files-found: ignore + retention-days: 30 + + - name: Fail the job if secrets were found + if: steps.scan.outputs.status != '0' + run: | + echo "::error::Secrets detected in PR diff. See the job summary." + exit 1 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..d4c422b --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,102 @@ +# Plaud-AI default gitleaks config. +# +# Place this file at the repo root (named `.gitleaks.toml`). The PR scan +# workflow (.github/workflows/secret-scan-pr.yml) reads it via --config. +# +# Strategy: +# - Inherit upstream gitleaks ruleset (covers AWS, GitHub, Slack, Stripe, +# OpenAI, Sentry, Twilio, Mailgun, JWT, generic high-entropy strings, etc.) +# - Add Plaud-wide allowlists for known noisy paths (lockfiles, fixtures) +# and placeholder values that appear in real code. +# +# Adding a new allowlist entry counts as a security change — open a PR and +# explain WHY this path/regex/commit is safe. Do not paste raw secrets in +# the description; reference the commit hash instead. + +title = "Plaud-AI gitleaks config" + +[extend] +useDefault = true + +# -- Lockfiles -------------------------------------------------------------- +# Lockfiles contain package URLs and sha256 hashes that gitleaks/trufflehog +# regularly mistake for Sentry / generic tokens. Real leaks via lockfiles +# happen only for private-registry URLs, which we ban at the registry layer +# (use a registry proxy, not embedded creds). +[[allowlists]] +description = "Dependency lockfiles" +paths = [ + '''(^|/)pnpm-lock\.ya?ml$''', + '''(^|/)package-lock\.json$''', + '''(^|/)yarn\.lock$''', + '''(^|/)poetry\.lock$''', + '''(^|/)uv\.lock$''', + '''(^|/)Cargo\.lock$''', + '''(^|/)go\.sum$''', + '''(^|/)composer\.lock$''', + '''(^|/)Gemfile\.lock$''', + '''(^|/)Pipfile\.lock$''', + '''(^|/)mix\.lock$''', +] + +# -- Test fixtures, examples, docs ------------------------------------------ +# Stuff in these locations is expected to contain fake credentials. +[[allowlists]] +description = "Test fixtures, examples, docs" +paths = [ + '''(^|/)testdata/''', + '''(^|/)fixtures?/''', + '''(^|/)__fixtures__/''', + '''(^|/)__mocks__/''', + '''(^|/)examples?/''', + '''(^|/)docs?/''', + '''(^|/)samples?/''', + '''\.(test|spec)\.(js|ts|tsx|jsx|py|go|rb|java|kt|swift)$''', +] + +# -- Build artifacts and generated output ----------------------------------- +[[allowlists]] +description = "Build artifacts and generated output" +paths = [ + '''(^|/)dist/''', + '''(^|/)build/''', + '''(^|/)out/''', + '''(^|/)\.next/''', + '''(^|/)\.nuxt/''', + '''(^|/)node_modules/''', + '''(^|/)vendor/''', + '''(^|/)target/''', + '''(^|/)\.gradle/''', + '''\.min\.(js|css)$''', + '''\.map$''', +] + +# -- Placeholder and obvious-fake values ------------------------------------ +[[allowlists]] +description = "Obvious placeholder values" +regexes = [ + '''(?i)\b(example|placeholder|dummy|fake|sample|your[-_]?(api[-_]?key|token|secret)|insert[-_]?your)\b''', + '''^(x{4,}|a{4,}|0{4,}|1{4,})$''', + '''123456(78)?9?0?''', + '''(?i)changeme|todo|fixme|tbd''', +] + +# -- Vendor-provided test keys (publishable by design) ---------------------- +# These are documented test keys from the vendor; safe to commit. +[[allowlists]] +description = "Vendor-published test keys (not real credentials)" +regexes = [ + '''sk_test_[A-Za-z0-9]+''', # Stripe test secret key (sandbox only) + '''pk_test_[A-Za-z0-9]+''', # Stripe test publishable key + '''AKIAIOSFODNN7EXAMPLE''', # AWS docs example access key id + '''wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY''', # AWS docs example secret +] + +# -- High-entropy generic detector tuning ----------------------------------- +# Disable / tune the generic-secret rule that produces the bulk of false +# positives on hex strings, base64 blobs, UUIDs, etc. +[[rules]] +id = "generic-api-key" +description = "Disabled — too noisy without verification; relies on context." +keywords = [] +regex = '''^$''' # match nothing