Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/secret-scan-pr.yml
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -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
Loading