#!/usr/bin/env bash set -Eeuo pipefail repository="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" cd "$repository" failures=0 report() { printf 'FAIL: %s\n' "$1" >&2 failures=$((failures + 1)) } scan_file() { local label=$1 file=$2 pattern=$3 if LC_ALL=C grep -IqiE -- "$pattern" "$file"; then report "$label in $file" fi } collect_matches() { local result_name=$1 label=$2 output status shift 2 if output="$(LC_ALL=C "$@" 2>/dev/null)"; then status=0 else status=$? fi if ((status > 1)); then report "$label could not complete" output='' fi printf -v "$result_name" '%s' "$output" } scan_network_literals() { local file=$1 context=$2 addresses domains address domain collect_matches addresses "IPv4 scan for $context$file" \ grep -IhoE -- '([0-9]{1,3}\.){3}[0-9]{1,3}' "$file" while IFS= read -r address; do [[ -z "$address" ]] && continue case "$address" in 127.0.0.1 | 0.0.0.0) ;; *) report "unexpected IPv4 literal $address in $context$file" ;; esac done <<<"$(printf '%s\n' "$addresses" | sort -u)" collect_matches domains "hostname scan for $context$file" \ grep -IhoE -- '([[:alnum:]][[:alnum:]-]*\.)+(com|org|net|ru|io|dev|app|cloud|info|biz|me|ai|local|localhost|de|uk|fr|us|ca|au|ch|nl|se|no|fi|pl|cz|kz|by|ua|su)([^[:alnum:]_.-]|$)' "$file" while IFS= read -r domain; do [[ -z "$domain" ]] && continue domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')" case "$domain" in github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \ gitea.sibdata.ru | \ example.com | *.example.com | *.localhost | \ *.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \ *.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;; *) report "unexpected hostname $domain in $context$file" ;; esac done <<<"$(printf '%s\n' "$domains" | sort -u)" } scan_local_denylist() { local file=$1 context=$2 literal [[ -f .audit-private-patterns ]] || return 0 while IFS= read -r literal || [[ -n "$literal" ]]; do [[ -z "$literal" || "$literal" == \#* ]] && continue if LC_ALL=C grep -IFqi -- "$literal" "$file"; then report "locally denied identifier in $context$file" fi done < .audit-private-patterns } # The scanner contains signatures for credentials, so it is excluded only from # those signature checks. Address, hostname and optional local deny-list checks # still inspect it. Every tracked or untracked file not ignored by Git is read. while IFS= read -r -d '' file; do [[ -f "$file" ]] || continue if [[ "$file" != scripts/audit-public.sh ]]; then scan_file "private-key material" "$file" \ '-----BEGIN ([A-Z0-9]+ )?PRIVATE KEY-----' scan_file "access-token pattern" "$file" \ '(ptr_|gh[pousr]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_+/=-]{16,}' scan_file "cloud credential pattern" "$file" \ '(AKIA|ASIA)[A-Z0-9]{16}|AIza[0-9A-Za-z_-]{35}' scan_file "credential embedded in URL" "$file" \ 'https?://[^/:[:space:]]+:[^/@[:space:]]+@' scan_file "Bearer token" "$file" \ 'Bearer[[:space:]]+[A-Za-z0-9._~+/-]{20,}={0,2}' scan_file "32-hex client secret" "$file" \ '(^|[^0-9a-f])[0-9a-f]{32}([^0-9a-f]|$)' scan_file "credential assignment" "$file" \ '(api[_-]?key|api[_-]?token|access[_-]?token|password|secret)[[:space:]]*[:=][[:space:]]*[^$[:space:]][A-Za-z0-9_+./=-]{15,}' fi scan_network_literals "$file" '' scan_local_denylist "$file" '' done < <(git ls-files --cached --others --exclude-standard -z) while IFS= read -r -d '' file; do case "$file" in .env.example) ;; .env | .env.* | */.env | */.env.* | *client-secret* | *profiles.json | \ *.pem | *.key | *.p12 | *.pfx | id_rsa | id_ed25519) report "sensitive filename is tracked: $file" ;; esac done < <(git ls-files -z) history_pattern='-----BEGIN ([A-Z0-9]+ )?PRIVATE KEY-----|(ptr_|gh[pousr]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_+/=-]{16,}|(AKIA|ASIA)[A-Z0-9]{16}|AIza[0-9A-Za-z_-]{35}|https?://[^/:[:space:]]+:[^/@[:space:]]+@|Bearer[[:space:]]+[A-Za-z0-9._~+/-]{20,}={0,2}|(^|[^0-9a-f])[0-9a-f]{32}([^0-9a-f]|$)|(api[_-]?key|api[_-]?token|access[_-]?token|password|secret)[[:space:]]*[:=][[:space:]]*[^$[:space:]][A-Za-z0-9_+./=-]{15,}' if revisions="$(git rev-list --all)"; then : else report "Git history could not be enumerated" revisions='' fi while IFS= read -r revision; do [[ -z "$revision" ]] && continue collect_matches matches "credential scan for Git history ${revision:0:12}" \ git grep -IilE -- "$history_pattern" "$revision" -- . \ ':(exclude)scripts/audit-public.sh' if [[ -n "$matches" ]]; then while IFS= read -r match; do report "sensitive content in Git history: $match" done <<<"$matches" fi collect_matches addresses "IPv4 scan for Git history ${revision:0:12}" \ git grep -IhoE -- '([0-9]{1,3}\.){3}[0-9]{1,3}' "$revision" -- . while IFS= read -r address; do [[ -z "$address" ]] && continue case "$address" in 127.0.0.1 | 0.0.0.0) ;; *) report "unexpected IPv4 literal $address in Git history ${revision:0:12}" ;; esac done <<<"$(printf '%s\n' "$addresses" | sort -u)" collect_matches domains "hostname scan for Git history ${revision:0:12}" \ git grep -IhoE -- '([[:alnum:]][[:alnum:]-]*\.)+(com|org|net|ru|io|dev|app|cloud|info|biz|me|ai|local|localhost|de|uk|fr|us|ca|au|ch|nl|se|no|fi|pl|cz|kz|by|ua|su)([^[:alnum:]_.-]|$)' "$revision" -- . while IFS= read -r domain; do [[ -z "$domain" ]] && continue domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')" case "$domain" in github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \ gitea.sibdata.ru | \ example.com | *.example.com | *.localhost | \ *.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \ *.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;; *) report "unexpected hostname $domain in Git history ${revision:0:12}" ;; esac done <<<"$(printf '%s\n' "$domains" | sort -u)" done <<<"$revisions" while IFS= read -r identity_email; do [[ -z "$identity_email" || "$identity_email" == *noreply* ]] && continue report "personal author or committer e-mail remains in Git history" break done < <(git log --all --format='%ae%n%ce' | sort -u) while IFS= read -r identity_name; do [[ -z "$identity_name" || "$identity_name" == Maintainer ]] && continue report "non-generic author or committer name remains in Git history" break done < <(git log --all --format='%an%n%cn' | sort -u) if ((failures > 0)); then printf '\nPublic-release audit failed with %d finding(s).\n' "$failures" >&2 exit 1 fi echo "Public-release audit passed: no secrets, deployment identifiers or unexpected address literals found."