Last active
March 25, 2026 10:45
-
-
Save ustun/ece8a17ab61028786c1366127cad1569 to your computer and use it in GitHub Desktop.
Find Plaintext Secrets Hiding in Your `.env` Files: https://dev.to/ustun/find-plaintext-secrets-hiding-in-your-env-files-5dpl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| ROOT="${HOME}" | |
| PATTERN="token|api|key" | |
| usage() { | |
| cat <<'EOF' | |
| Scan for env files under a root directory that contain sensitive-looking patterns. | |
| Usage: | |
| scripts/find-env-secret-patterns.sh [options] | |
| Options: | |
| --root DIR Root directory to scan (default: $HOME) | |
| --pattern REGEX Case-insensitive regex to search for (default: token|api|key) | |
| -h, --help Show this help | |
| Scanned filenames: | |
| .env | |
| .env.production | |
| .env.local | |
| .env.local.secrets | |
| Ignored directories: | |
| node_modules, .git, .hg, .svn, .next, .turbo, dist, build, coverage, | |
| .cache, .npm, .pnpm-store, .yarn, Library, Trash | |
| Examples: | |
| scripts/find-env-secret-patterns.sh | |
| scripts/find-env-secret-patterns.sh --root ~/code | |
| scripts/find-env-secret-patterns.sh --pattern 'token|secret|api|key' | |
| Notes: | |
| Matching lines containing 'encrypted:', 'DOTENV_PUBLIC_KEY', or | |
| 'public-key encryption for .env files', or | |
| 'USE_KEYCHAIN_FOR_DOTX' are ignored. | |
| `.env.local.secrets` reports only env-style `key=value` lines that do | |
| not contain `encrypted:`. | |
| Reported matches redact everything after the first '='. | |
| EOF | |
| } | |
| is_env_local_secrets_file() { | |
| local file_path="$1" | |
| [[ "${file_path##*/}" == ".env.local.secrets" ]] | |
| } | |
| should_skip_match_line() { | |
| local line_content_lower="${1,,}" | |
| local skip_pattern | |
| for skip_pattern in \ | |
| "encrypted:" \ | |
| "dotenv_public_key" \ | |
| "public-key encryption for .env files" \ | |
| "use_keychain_for_dotx"; do | |
| if [[ "$line_content_lower" == *"$skip_pattern"* ]]; then | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| is_env_assignment_line() { | |
| local line_content="$1" | |
| [[ "$line_content" =~ ^[[:space:]#]*((export[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=) ]] | |
| } | |
| redact_match_line() { | |
| local line_content="$1" | |
| if [[ "$line_content" == *"="* ]]; then | |
| printf '%s[REDACTED]' "${line_content%%=*}=" | |
| return | |
| fi | |
| printf '[REDACTED]' | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --root) | |
| ROOT="${2:-}" | |
| shift 2 | |
| ;; | |
| --pattern) | |
| PATTERN="${2:-}" | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" >&2 | |
| usage >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| if [[ -z "$ROOT" ]]; then | |
| echo "Error: --root cannot be empty." >&2 | |
| exit 1 | |
| fi | |
| if [[ -z "$PATTERN" ]]; then | |
| echo "Error: --pattern cannot be empty." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! -d "$ROOT" ]]; then | |
| echo "Error: root directory not found: $ROOT" >&2 | |
| exit 1 | |
| fi | |
| MATCHING_FILES="$(mktemp)" | |
| trap 'rm -f "$MATCHING_FILES"' EXIT | |
| find "$ROOT" \ | |
| \( \ | |
| -name node_modules -o \ | |
| -name .git -o \ | |
| -name .hg -o \ | |
| -name .svn -o \ | |
| -name .next -o \ | |
| -name .turbo -o \ | |
| -name dist -o \ | |
| -name build -o \ | |
| -name coverage -o \ | |
| -name .cache -o \ | |
| -name .npm -o \ | |
| -name .pnpm-store -o \ | |
| -name .yarn -o \ | |
| -name Library -o \ | |
| -name .Trash -o \ | |
| -name Trash \ | |
| \) -prune \ | |
| -o -type f \ | |
| \( \ | |
| -name .env -o \ | |
| -name .env.production -o \ | |
| -name .env.local -o \ | |
| -name .env.local.secrets \ | |
| \) -print0 >"$MATCHING_FILES" | |
| if [[ ! -s "$MATCHING_FILES" ]]; then | |
| echo "No matching env files found under $ROOT." | |
| exit 0 | |
| fi | |
| FOUND_MATCH=0 | |
| HAD_ERROR=0 | |
| while IFS= read -r -d '' file_path; do | |
| set +e | |
| RG_OUTPUT="$( | |
| rg \ | |
| --ignore-case \ | |
| --line-number \ | |
| --color never \ | |
| -- "$PATTERN" "$file_path" 2>&1 | |
| )" | |
| RG_STATUS=$? | |
| set -e | |
| case "$RG_STATUS" in | |
| 0) | |
| while IFS= read -r rg_line; do | |
| [[ -z "$rg_line" ]] && continue | |
| line_number="${rg_line%%:*}" | |
| line_content="${rg_line#*:}" | |
| if should_skip_match_line "$line_content"; then | |
| continue | |
| fi | |
| if is_env_local_secrets_file "$file_path" && ! is_env_assignment_line "$line_content"; then | |
| continue | |
| fi | |
| printf '%s:%s:%s\n' "$file_path" "$line_number" "$(redact_match_line "$line_content")" | |
| FOUND_MATCH=1 | |
| done <<<"$RG_OUTPUT" | |
| ;; | |
| 1) | |
| ;; | |
| *) | |
| printf 'Error scanning %s\n%s\n' "$file_path" "$RG_OUTPUT" >&2 | |
| HAD_ERROR=1 | |
| ;; | |
| esac | |
| done <"$MATCHING_FILES" | |
| if [[ "$HAD_ERROR" -ne 0 ]]; then | |
| exit 1 | |
| fi | |
| if [[ "$FOUND_MATCH" -eq 1 ]]; then | |
| exit 0 | |
| fi | |
| echo "No matching lines found for pattern '$PATTERN' under $ROOT." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment