Files
gh-dhofheinz-open-plugins-p…/commands/security-scan/.scripts/secret-scanner.sh
2025-11-29 18:20:28 +08:00

417 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# Secret Scanner - Detect exposed secrets with 50+ patterns
# ============================================================================
# Purpose: Comprehensive secret detection for API keys, tokens, credentials
# Version: 1.0.0
# Usage: ./secret-scanner.sh <path> <recursive> <patterns> <exclude> <severity>
# Returns: 0=no secrets, 1=secrets found, 2=error
# ============================================================================
set -euo pipefail
# Source shared validation library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
if [[ -f "${PLUGIN_ROOT}/scripts/validate-lib.sh" ]]; then
source "${PLUGIN_ROOT}/scripts/validate-lib.sh"
fi
# ============================================================================
# Configuration
# ============================================================================
# Default values
PATH_TO_SCAN="${1:-.}"
RECURSIVE="${2:-true}"
PATTERNS="${3:-all}"
EXCLUDE="${4:-}"
MIN_SEVERITY="${5:-medium}"
SECRETS_FOUND=0
declare -a FINDINGS=()
# ============================================================================
# Secret Pattern Definitions (50+ patterns)
# ============================================================================
# API Keys & Service Tokens
declare -A API_KEY_PATTERNS=(
# Stripe
["stripe_live_key"]='sk_live_[a-zA-Z0-9]{24,}'
["stripe_test_key"]='sk_test_[a-zA-Z0-9]{24,}'
["stripe_publishable_live"]='pk_live_[a-zA-Z0-9]{24,}'
["stripe_publishable_test"]='pk_test_[a-zA-Z0-9]{24,}'
# OpenAI
["openai_api_key"]='sk-[a-zA-Z0-9]{32,}'
# AWS
["aws_access_key_id"]='AKIA[0-9A-Z]{16}'
["aws_secret_access_key"]='aws_secret_access_key.*[=:].*[A-Za-z0-9/+=]{40}'
# Google
["google_api_key"]='AIza[0-9A-Za-z_-]{35}'
["google_oauth_id"]='[0-9]+-[0-9A-Za-z_-]{32}\.apps\.googleusercontent\.com'
# GitHub
["github_personal_token"]='ghp_[a-zA-Z0-9]{36}'
["github_oauth_token"]='gho_[a-zA-Z0-9]{36}'
["github_app_token"]='ghs_[a-zA-Z0-9]{36}'
["github_user_token"]='ghu_[a-zA-Z0-9]{36}'
["github_refresh_token"]='ghr_[a-zA-Z0-9]{36}'
# Slack
["slack_token"]='xox[baprs]-[0-9a-zA-Z]{10,}'
["slack_webhook"]='https://hooks\.slack\.com/services/T[0-9A-Z]{8}/B[0-9A-Z]{8}/[0-9A-Za-z]{24}'
# Twitter
["twitter_access_token"]='[0-9]{15,}-[0-9a-zA-Z]{35,44}'
["twitter_api_key"]='[A-Za-z0-9]{25}'
["twitter_api_secret"]='[A-Za-z0-9]{50}'
# Facebook
["facebook_access_token"]='EAA[0-9A-Za-z]{90,}'
# SendGrid
["sendgrid_api_key"]='SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}'
# Mailgun
["mailgun_api_key"]='key-[0-9a-zA-Z]{32}'
# Twilio
["twilio_account_sid"]='AC[a-f0-9]{32}'
["twilio_api_key"]='SK[a-f0-9]{32}'
# Azure
["azure_storage_key"]='[a-zA-Z0-9/+=]{88}'
["azure_connection_string"]='AccountKey=[a-zA-Z0-9/+=]{88}'
# Generic patterns
["generic_api_key"]='api[_-]?key.*[=:].*['\''"][a-zA-Z0-9]{20,}['\''"]'
["generic_secret"]='secret.*[=:].*['\''"][a-zA-Z0-9]{20,}['\''"]'
["generic_token"]='token.*[=:].*['\''"][a-zA-Z0-9]{20,}['\''"]'
["generic_password"]='password.*[=:].*['\''"][^'\''\"]{8,}['\''"]'
["bearer_token"]='Bearer [a-zA-Z0-9_-]{20,}'
["authorization_header"]='Authorization.*Basic [a-zA-Z0-9+/=]{20,}'
)
# Private Keys
declare -A PRIVATE_KEY_PATTERNS=(
["rsa_private_key"]='-----BEGIN RSA PRIVATE KEY-----'
["openssh_private_key"]='-----BEGIN OPENSSH PRIVATE KEY-----'
["private_key_generic"]='-----BEGIN PRIVATE KEY-----'
["pgp_private_key"]='-----BEGIN PGP PRIVATE KEY BLOCK-----'
["dsa_private_key"]='-----BEGIN DSA PRIVATE KEY-----'
["ec_private_key"]='-----BEGIN EC PRIVATE KEY-----'
["encrypted_private_key"]='-----BEGIN ENCRYPTED PRIVATE KEY-----'
)
# Cloud Provider Credentials
declare -A CLOUD_PATTERNS=(
["aws_credentials_block"]='aws_access_key_id|aws_secret_access_key'
["gcp_service_account"]='type.*service_account'
["azure_client_secret"]='client_secret.*[=:].*[a-zA-Z0-9~._-]{34,}'
["heroku_api_key"]='[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
)
# Database Connection Strings
declare -A DATABASE_PATTERNS=(
["mongodb_connection"]='mongodb(\+srv)?://[^:]+:[^@]+@'
["postgres_connection"]='postgres(ql)?://[^:]+:[^@]+@'
["mysql_connection"]='mysql://[^:]+:[^@]+@'
["redis_connection"]='redis://[^:]+:[^@]+@'
)
# ============================================================================
# Severity Classification
# ============================================================================
get_pattern_severity() {
local pattern_name="$1"
case "${pattern_name}" in
# CRITICAL: Private keys, production credentials
*_private_key*|aws_access_key_id|aws_secret_access_key|*_connection)
echo "critical"
;;
# HIGH: Service API keys, OAuth tokens
stripe_live_key|openai_api_key|github_*_token|slack_token|*_access_token)
echo "high"
;;
# MEDIUM: Passwords, secrets, test keys
*_password|*_secret|stripe_test_key|generic_*)
echo "medium"
;;
# LOW: Everything else
*)
echo "low"
;;
esac
}
# ============================================================================
# Pattern Filtering
# ============================================================================
should_check_pattern() {
local pattern_name="$1"
local severity
severity=$(get_pattern_severity "${pattern_name}")
# Check if pattern category requested
if [[ "${PATTERNS}" != "all" ]]; then
case "${PATTERNS}" in
*api-keys*) [[ "${pattern_name}" =~ _api_key|_token ]] || return 1 ;;
*private-keys*) [[ "${pattern_name}" =~ private_key ]] || return 1 ;;
*passwords*) [[ "${pattern_name}" =~ password ]] || return 1 ;;
*cloud*) [[ "${pattern_name}" =~ aws_|gcp_|azure_ ]] || return 1 ;;
esac
fi
# Check severity threshold
case "${MIN_SEVERITY}" in
critical)
[[ "${severity}" == "critical" ]] || return 1
;;
high)
[[ "${severity}" == "critical" || "${severity}" == "high" ]] || return 1
;;
medium)
[[ "${severity}" != "low" ]] || return 1
;;
low)
# Report all
;;
esac
return 0
}
# ============================================================================
# File Exclusion
# ============================================================================
should_exclude_file() {
local file="$1"
# Default exclusions
if [[ "${file}" =~ \.(git|node_modules|vendor|dist|build)/ ]]; then
return 0
fi
# User-specified exclusions
if [[ -n "${EXCLUDE}" ]]; then
IFS=',' read -ra EXCLUDE_PATTERNS <<< "${EXCLUDE}"
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
if [[ "${file}" =~ ${pattern} ]]; then
return 0
fi
done
fi
return 1
}
# ============================================================================
# Secret Scanning
# ============================================================================
scan_file() {
local file="$1"
local file_findings=0
# Skip excluded files
if should_exclude_file "${file}"; then
return 0
fi
# Skip binary files
if file "${file}" 2>/dev/null | grep -q "text"; then
:
else
return 0
fi
# Scan with all pattern categories
for pattern_name in "${!API_KEY_PATTERNS[@]}"; do
if should_check_pattern "${pattern_name}"; then
local pattern="${API_KEY_PATTERNS[${pattern_name}]}"
if grep -nE "${pattern}" "${file}" &>/dev/null; then
local severity
severity=$(get_pattern_severity "${pattern_name}")
local line_numbers
line_numbers=$(grep -nE "${pattern}" "${file}" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//')
FINDINGS+=("${severity}|${file}|${line_numbers}|${pattern_name}|API Key")
((file_findings++))
fi
fi
done
for pattern_name in "${!PRIVATE_KEY_PATTERNS[@]}"; do
if should_check_pattern "${pattern_name}"; then
local pattern="${PRIVATE_KEY_PATTERNS[${pattern_name}]}"
if grep -nF "${pattern}" "${file}" &>/dev/null; then
local severity
severity=$(get_pattern_severity "${pattern_name}")
local line_numbers
line_numbers=$(grep -nF "${pattern}" "${file}" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//')
FINDINGS+=("critical|${file}|${line_numbers}|${pattern_name}|Private Key")
((file_findings++))
fi
fi
done
for pattern_name in "${!CLOUD_PATTERNS[@]}"; do
if should_check_pattern "${pattern_name}"; then
local pattern="${CLOUD_PATTERNS[${pattern_name}]}"
if grep -nE "${pattern}" "${file}" &>/dev/null; then
local severity
severity=$(get_pattern_severity "${pattern_name}")
local line_numbers
line_numbers=$(grep -nE "${pattern}" "${file}" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//')
FINDINGS+=("${severity}|${file}|${line_numbers}|${pattern_name}|Cloud Credential")
((file_findings++))
fi
fi
done
for pattern_name in "${!DATABASE_PATTERNS[@]}"; do
if should_check_pattern "${pattern_name}"; then
local pattern="${DATABASE_PATTERNS[${pattern_name}]}"
if grep -nE "${pattern}" "${file}" &>/dev/null; then
FINDINGS+=("critical|${file}|$(grep -nE "${pattern}" "${file}" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//')|${pattern_name}|Database Connection")
((file_findings++))
fi
fi
done
((SECRETS_FOUND += file_findings))
return 0
}
# ============================================================================
# Main Execution
# ============================================================================
main() {
# Validate path
if [[ ! -e "${PATH_TO_SCAN}" ]]; then
echo "ERROR: Path does not exist: ${PATH_TO_SCAN}" >&2
exit 2
fi
echo "Secret Scanner"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Path: ${PATH_TO_SCAN}"
echo "Recursive: ${RECURSIVE}"
echo "Min Severity: ${MIN_SEVERITY}"
echo "Patterns: 50+"
echo ""
# Scan files
local files_scanned=0
if [[ -f "${PATH_TO_SCAN}" ]]; then
# Single file
scan_file "${PATH_TO_SCAN}"
((files_scanned++))
elif [[ -d "${PATH_TO_SCAN}" ]]; then
# Directory
if [[ "${RECURSIVE}" == "true" ]]; then
while IFS= read -r -d '' file; do
scan_file "${file}"
((files_scanned++))
done < <(find "${PATH_TO_SCAN}" -type f -print0)
else
while IFS= read -r file; do
scan_file "${file}"
((files_scanned++))
done < <(find "${PATH_TO_SCAN}" -maxdepth 1 -type f)
fi
fi
echo "Files Scanned: ${files_scanned}"
echo ""
# Report findings
if [[ ${SECRETS_FOUND} -eq 0 ]]; then
echo "✅ SUCCESS: No secrets detected"
echo "All files clean"
exit 0
fi
echo "⚠️ SECRETS DETECTED: ${SECRETS_FOUND}"
echo ""
# Group by severity
local critical_count=0
local high_count=0
local medium_count=0
local low_count=0
for finding in "${FINDINGS[@]}"; do
IFS='|' read -r severity file lines pattern type <<< "${finding}"
case "${severity}" in
critical) ((critical_count++)) ;;
high) ((high_count++)) ;;
medium) ((medium_count++)) ;;
low) ((low_count++)) ;;
esac
done
# Print findings by severity
if [[ ${critical_count} -gt 0 ]]; then
echo "CRITICAL Issues (${critical_count}):"
for finding in "${FINDINGS[@]}"; do
IFS='|' read -r severity file lines pattern type <<< "${finding}"
if [[ "${severity}" == "critical" ]]; then
echo "${file}:${lines}"
echo " Type: ${type}"
echo " Pattern: ${pattern}"
echo " Remediation: Remove and rotate immediately"
echo ""
fi
done
fi
if [[ ${high_count} -gt 0 ]]; then
echo "HIGH Issues (${high_count}):"
for finding in "${FINDINGS[@]}"; do
IFS='|' read -r severity file lines pattern type <<< "${finding}"
if [[ "${severity}" == "high" ]]; then
echo " ⚠️ ${file}:${lines}"
echo " Type: ${type}"
echo " Pattern: ${pattern}"
echo ""
fi
done
fi
if [[ ${medium_count} -gt 0 ]]; then
echo "MEDIUM Issues (${medium_count}):"
for finding in "${FINDINGS[@]}"; do
IFS='|' read -r severity file lines pattern type <<< "${finding}"
if [[ "${severity}" == "medium" ]]; then
echo " 💡 ${file}:${lines}"
echo " Type: ${type}"
echo ""
fi
done
fi
echo "Summary:"
echo " Critical: ${critical_count}"
echo " High: ${high_count}"
echo " Medium: ${medium_count}"
echo " Low: ${low_count}"
echo ""
echo "Action Required: YES"
exit 1
}
main "$@"