-
Notifications
You must be signed in to change notification settings - Fork 0
Add stack secret scanning guardrails #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| name: Security scan | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| env: | ||
| GITLEAKS_VERSION: v8.30.1 | ||
|
|
||
| jobs: | ||
| gitleaks: | ||
| name: Gitleaks | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version: "1.24.11" | ||
| cache: false | ||
|
|
||
| - name: Scan current tree for secrets | ||
| run: go run github.com/zricethezav/gitleaks/v8@${GITLEAKS_VERSION} dir --redact --config .gitleaks.toml . | ||
|
|
||
| stack-sensitive-content: | ||
| name: Stack sensitive content | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Scan stack-specific sensitive content | ||
| run: scripts/security/scan-sensitive-content.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| config/settings.local.json | ||
| node_modules/ | ||
| .DS_Store | ||
| gitleaks-report*.json | ||
| gitleaks-findings*.json | ||
| trufflehog-report*.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| title = "Stack secret scanning" | ||
| minVersion = "v8.25.0" | ||
|
|
||
| [extend] | ||
| useDefault = true | ||
|
|
||
| [[rules]] | ||
| id = "stack-local-maroun-path" | ||
| description = "Local Maroun machine paths should not be published in the stack repo." | ||
| regex = '''/Users/maroun/[A-Za-z0-9._~/%+-]+''' | ||
| keywords = ["/Users/maroun"] | ||
|
|
||
| [[rules]] | ||
| id = "stack-codex-private-artifact" | ||
| description = "Codex session, memory, and rollout artifacts can contain private context." | ||
| regex = '''(?i)(?:\.codex/(?:sessions|archived_sessions|memories)|rollout-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9-]+-[0-9a-f-]+\.jsonl)''' | ||
| keywords = [".codex", "rollout-"] | ||
|
|
||
| [[rules]] | ||
| id = "stack-config-secret-assignment" | ||
| description = "Agent or tool config appears to contain a concrete secret value." | ||
| regex = '''(?i)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|private[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9_./+=-]{16,}''' | ||
| keywords = ["api", "key", "token", "secret", "password"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| default_install_hook_types: [pre-commit, pre-push] | ||
|
|
||
| repos: | ||
| - repo: https://github.com/gitleaks/gitleaks | ||
| rev: v8.30.1 | ||
| hooks: | ||
| - id: gitleaks | ||
| name: Gitleaks staged secret scan | ||
| stages: [pre-commit] | ||
|
|
||
| - repo: local | ||
| hooks: | ||
| - id: stack-sensitive-content | ||
| name: Stack sensitive content scan | ||
| entry: scripts/security/scan-sensitive-content.sh | ||
| language: script | ||
| pass_filenames: false | ||
| stages: [pre-commit, pre-push] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Leak Prevention | ||
|
|
||
| This repo is designed to be shared, so every pull request should pass two layers | ||
| of secret and sensitive-content checks before merge. | ||
|
|
||
| ## Required PR Gate | ||
|
|
||
| The `Security scan` GitHub Actions workflow runs on pull requests and on pushes | ||
| to `main`. | ||
|
|
||
| - `Gitleaks` catches provider tokens, private keys, connection strings, and other | ||
| standard secret patterns in the current tree. | ||
| - `Stack sensitive content` catches repo-specific risks such as local machine | ||
| paths, Codex session artifacts, credential-like agent config fields, and | ||
| household or finance-lane references. | ||
|
|
||
| Scanner output intentionally reports only file, line, and rule name. Do not paste | ||
| the matched secret value into chat, issues, pull request comments, or commit | ||
| messages while fixing a failure. | ||
|
|
||
| ## Optional Local Hook | ||
|
|
||
| Install local hooks when working on this repo: | ||
|
|
||
| ```bash | ||
| cd ~/Projects/stack | ||
| pre-commit install | ||
| ``` | ||
|
|
||
| Then run the checks manually before opening a PR: | ||
|
|
||
| ```bash | ||
| pre-commit run --all-files | ||
| scripts/security/scan-sensitive-content.sh | ||
| go run github.com/zricethezav/gitleaks/v8@v8.30.1 dir --redact --config .gitleaks.toml . | ||
| ``` | ||
|
|
||
| The local hook is a convenience layer. GitHub Actions remains the enforced gate. | ||
|
|
||
| For a one-time audit of older commits, run Gitleaks in git-history mode: | ||
|
|
||
| ```bash | ||
| go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --redact --config .gitleaks.toml . | ||
| ``` | ||
|
|
||
| Treat real credentials in history as leaked and rotate them. Historical local | ||
| path findings should be cleaned from the current tree, but they do not require | ||
| credential rotation. | ||
|
|
||
| ## If A Leak Is Found | ||
|
|
||
| 1. Remove the sensitive value from the branch. | ||
| 2. Rotate or revoke the credential if a real token, password, private key, or | ||
| session artifact was committed or pushed. | ||
| 3. Keep remediation notes generic: name the provider and file path, not the | ||
| secret value. | ||
| 4. Re-run the scanners before requesting review. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" | ||
| cd "$repo_root" | ||
|
|
||
| failures=0 | ||
|
|
||
| ignored_path() { | ||
| case "$1" in | ||
| .git/*|node_modules/*|scripts/security/scan-sensitive-content.sh|.gitleaks.toml) | ||
| return 0 | ||
| ;; | ||
| *) | ||
| return 1 | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| binary_or_unreadable() { | ||
| [[ ! -f "$1" ]] && return 0 | ||
| grep -Iq . "$1" 2>/dev/null | ||
| } | ||
|
|
||
| report() { | ||
| local file="$1" | ||
| local line="$2" | ||
| local rule="$3" | ||
|
|
||
| printf '%s:%s: sensitive-content: %s\n' "$file" "$line" "$rule" >&2 | ||
| failures=$((failures + 1)) | ||
| } | ||
|
|
||
| scan_path_name() { | ||
| local file="$1" | ||
|
|
||
| case "$file" in | ||
| *.pem|*.p12|*.pfx|*.key|*.asc|*.kdbx|*.mobileconfig) | ||
| report "$file" 0 "private key, certificate, or credential-like file extension" | ||
| ;; | ||
| .env|.env.*|*/.env|*/.env.*|*.env) | ||
| case "$file" in | ||
| *.example|*.sample|*.template) | ||
| ;; | ||
| *) | ||
| report "$file" 0 "environment file should not be committed" | ||
| ;; | ||
| esac | ||
| ;; | ||
| */settings.local.json|settings.local.json|*.local.json|*.secrets.*|*secret*.json|*credentials*.json) | ||
| report "$file" 0 "local settings or credential-like file name" | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| scan_line() { | ||
| local file="$1" | ||
| local findings | ||
| local count | ||
|
|
||
| findings="$( | ||
| awk -v file="$file" ' | ||
| function placeholder(line) { | ||
| line = tolower(line) | ||
| return line ~ /\$\{\{[^}]*secrets\.[^}]*}}/ || | ||
| line ~ /<your_|<example|example_|changeme|replace_me|not-a-real|redacted|placeholder/ | ||
| } | ||
|
|
||
| function finding(rule) { | ||
| print file ":" NR ": sensitive-content: " rule | ||
| } | ||
|
|
||
| { | ||
| lower = tolower($0) | ||
|
|
||
| if ($0 ~ /-----BEGIN[[:space:]][A-Z0-9[:space:]]*PRIVATE[[:space:]]KEY-----/) { | ||
| finding("private key material") | ||
| } | ||
|
|
||
| if ($0 ~ /https?:\/\/[^[:space:]\/:@]+:[^[:space:]@]+@/) { | ||
| finding("URL contains embedded credentials") | ||
| } | ||
|
|
||
| if ($0 ~ /\/Users\/maroun\//) { | ||
| finding("local Maroun machine path") | ||
| } | ||
|
|
||
| if ($0 ~ /\.codex\/(sessions|archived_sessions|memories)/ || | ||
| $0 ~ /rollout-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9-]+-[0-9a-f-]+\.jsonl/) { | ||
| finding("private Codex session or memory artifact") | ||
| } | ||
|
|
||
| if (lower ~ /(finance\/amazon|amazon-collette|account_snapshots|monarch|copilot[[:space:]_-]*account)/) { | ||
| finding("personal finance or household account lane") | ||
| } | ||
|
|
||
| if (!placeholder($0) && | ||
| $0 ~ /(sk-ant-api03-[A-Za-z0-9_-]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{32,}|gh[pousr]_[A-Za-z0-9_]{32,}|github_pat_[A-Za-z0-9_]{32,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|xox[baprs]-[A-Za-z0-9-]{20,})/) { | ||
| finding("provider token pattern") | ||
| } | ||
|
|
||
| if (!placeholder($0) && | ||
| lower ~ /(api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\''"]?[a-z0-9_.\/+=-]{16,}/) { | ||
| finding("secret-looking config assignment") | ||
| } | ||
| } | ||
| ' "$file" | ||
| )" | ||
|
|
||
| if [[ -n "$findings" ]]; then | ||
| printf '%s\n' "$findings" >&2 | ||
| count="$(printf '%s\n' "$findings" | wc -l | tr -d ' ')" | ||
| failures=$((failures + count)) | ||
| fi | ||
| } | ||
|
|
||
| scan_file() { | ||
| local file="$1" | ||
|
|
||
| ignored_path "$file" && return 0 | ||
| binary_or_unreadable "$file" || return 0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Useful? React with 👍 / 👎. |
||
|
|
||
| scan_path_name "$file" | ||
| scan_line "$file" | ||
| } | ||
|
|
||
| while IFS= read -r -d '' file; do | ||
| scan_file "$file" | ||
| done < <(git ls-files -co --exclude-standard -z) | ||
|
|
||
| if (( failures > 0 )); then | ||
| printf '\nStack sensitive content scan failed with %s finding(s).\n' "$failures" >&2 | ||
| printf 'Review the flagged lines without pasting secret values into chat or PR comments.\n' >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| printf 'Stack sensitive content scan passed.\n' | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assignment regex requires
:or=immediately after the key name, so a normal JSON config line like"api_key": "..."has a quote betweenapi_keyand:and is not flagged. In a PR adding generic tokens/passwords to JSON files that do not match a provider-specific pattern, both the stack scanner and the matching Gitleaks rule miss the secret-looking assignment; allow an optional closing quote before the separator.Useful? React with 👍 / 👎.