Initial commit
This commit is contained in:
416
commands/security-scan/.scripts/secret-scanner.sh
Executable file
416
commands/security-scan/.scripts/secret-scanner.sh
Executable file
@@ -0,0 +1,416 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user