Detects hardcoded secrets, API keys, and credentials in source code.
Identifies exposed secrets before they reach version control.
python secret_scanner.py /path/to/project
python secret_scanner.py /path/to/file.py
python secret_scanner.py /path/to/project --format json
python secret_scanner.py --list-patterns
from dataclasses import dataclass
from typing import Dict, List, Optional
file_extensions: List[str]
# Secret patterns database
name="AWS Access Key ID",
description="AWS access key identifier",
regex=r'AKIA[0-9A-Z]{16}',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json", ".xml", ".conf"],
recommendation="Use IAM roles or AWS Secrets Manager instead of hardcoded keys"
name="AWS Secret Access Key",
description="AWS secret access key",
regex=r'(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*["\']?[A-Za-z0-9/+=]{40}["\']?',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json", ".conf"],
recommendation="Use IAM roles or AWS Secrets Manager instead of hardcoded secrets"
name="Google Cloud API Key",
description="Google Cloud Platform API key",
regex=r'AIza[0-9A-Za-z\-_]{35}',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use service accounts or Google Secret Manager"
name="Azure Storage Key",
description="Azure storage account key",
regex=r'(?:AccountKey|account_key)\s*[:=]\s*["\']?[A-Za-z0-9+/=]{88}["\']?',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".cs", ".env", ".yml", ".yaml", ".json"],
recommendation="Use Azure Key Vault or managed identities"
description="Hardcoded JWT token",
regex=r'eyJ[A-Za-z0-9-_=]+\.eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_.+/=]*',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".json"],
recommendation="Generate tokens dynamically, never hardcode"
description="GitHub personal access token or OAuth token",
regex=r'(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use GitHub App authentication or environment variables"
description="GitLab personal access or pipeline token",
regex=r'glpat-[A-Za-z0-9\-_]{20,}',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml"],
recommendation="Use CI/CD variables or environment variables"
description="Slack API token",
regex=r'xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use environment variables or secrets manager"
description="Stripe secret or publishable key",
regex=r'(?:sk|pk)_(?:test|live)_[0-9a-zA-Z]{24,}',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use environment variables, never commit API keys"
description="Twilio account SID or auth token",
regex=r'(?:AC[a-z0-9]{32}|SK[a-z0-9]{32})',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use environment variables for Twilio credentials"
pattern_id="SENDGRID001",
description="SendGrid API key",
regex=r'SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use environment variables for email service credentials"
description="RSA private key in PEM format",
regex=r'-----BEGIN RSA PRIVATE KEY-----',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".pem", ".key", ".txt"],
recommendation="Store private keys in secure key management systems"
description="Elliptic curve private key",
regex=r'-----BEGIN EC PRIVATE KEY-----',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".pem", ".key"],
recommendation="Use hardware security modules or key management services"
name="OpenSSH Private Key",
description="OpenSSH private key",
regex=r'-----BEGIN OPENSSH PRIVATE KEY-----',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".pem", ".key", ".txt"],
recommendation="Never commit SSH keys to repositories"
description="PGP/GPG private key block",
regex=r'-----BEGIN PGP PRIVATE KEY BLOCK-----',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".asc", ".gpg", ".txt"],
recommendation="Store PGP keys in secure key rings, not source code"
description="Generic API key or secret pattern",
regex=r'(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{20,}["\']',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json", ".xml"],
recommendation="Use environment variables or secrets manager"
description="Generic secret or token pattern",
regex=r'(?:secret|token|auth[_-]?token)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{20,}["\']',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Store secrets in environment variables or secret managers"
name="Password in Config",
description="Password in configuration file",
regex=r'(?:password|passwd|pwd)\s*[:=]\s*["\'][^"\']{8,}["\']',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json", ".xml", ".conf", ".ini"],
recommendation="Never hardcode passwords. Use secret managers"
name="Database Connection String",
description="Database connection string with credentials",
regex=r'(?:mongodb|postgres|mysql|redis|amqp)://[^:]+:[^@]+@[^/]+',
severity=Severity.CRITICAL,
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".env", ".yml", ".yaml", ".json"],
recommendation="Use environment variables for database credentials"
description="TODO comment mentioning secrets or credentials",
regex=r'(?:#|//|/\*)\s*(?:TODO|FIXME|XXX).*(?:secret|password|credential|key)',
file_extensions=[".py", ".js", ".ts", ".java", ".go", ".rb", ".php"],
recommendation="Address security TODOs before deployment"
def scan_file(file_path: Path, patterns: List[SecretPattern]) -> List[SecretFinding]:
"""Scan a single file for secrets."""
extension = file_path.suffix.lower()
content = file_path.read_text(encoding='utf-8', errors='ignore')
lines = content.split('\n')
if extension not in pattern.file_extensions:
regex = re.compile(pattern.regex, re.IGNORECASE)
for i, line in enumerate(lines, 1):
# Skip comments that explain patterns (like in this file)
if 'regex' in line.lower() or 'pattern' in line.lower():
match = regex.search(line)
# Mask the actual secret for safety
masked = matched[:10] + "..." + matched[-5:]
masked = matched[:5] + "..."
findings.append(SecretFinding(
pattern_id=pattern.pattern_id,
severity=pattern.severity,
file_path=str(file_path),
recommendation=pattern.recommendation
def scan_directory(dir_path: Path, patterns: List[SecretPattern],
exclude_dirs: List[str] = None) -> List[SecretFinding]:
"""Scan all files in a directory for secrets."""
"node_modules", ".git", "__pycache__", "venv", ".venv",
"dist", "build", ".next", "vendor", ".idea", ".vscode"
extensions.update(pattern.file_extensions)
for file_path in dir_path.rglob("*"):
if any(excluded in file_path.parts for excluded in exclude_dirs):
# Skip binary files and large files
if file_path.stat().st_size > 1_000_000: # 1MB limit
if file_path.suffix.lower() in extensions or file_path.name in ['.env', '.env.local', '.env.production']:
findings.extend(scan_file(file_path, patterns))
return sorted(findings, key=lambda f: (
0 if f.severity == Severity.CRITICAL else
1 if f.severity == Severity.HIGH else
2 if f.severity == Severity.MEDIUM else 3
def format_text_report(findings: List[SecretFinding], path: str) -> str:
"""Format findings as text report."""
lines.append("SECRET SCAN REPORT")
lines.append(f"Target: {path}")
sev = finding.severity.value
by_severity[sev] = by_severity.get(sev, 0) + 1
lines.append(f" Total Secrets Found: {len(findings)}")
for sev in ["critical", "high", "medium", "low"]:
count = by_severity.get(sev, 0)
lines.append(f" {sev.upper()}: {count}")
lines.append("No secrets found!")
if finding.severity != current_severity:
current_severity = finding.severity
lines.append(f"[{current_severity.value.upper()}]")
lines.append(f" [{finding.pattern_id}] {finding.name}")
lines.append(f" File: {finding.file_path}:{finding.line_number}")
lines.append(f" Match: {finding.matched_text}")
lines.append(f" Fix: {finding.recommendation}")
lines.append("IMPORTANT: Review all findings and rotate exposed credentials!")
def format_json_report(findings: List[SecretFinding], path: str) -> Dict:
"""Format findings as JSON."""
"scan_date": __import__('datetime').datetime.now().isoformat(),
sev.value: sum(1 for f in findings if f.severity == sev)
"pattern_id": f.pattern_id,
"severity": f.severity.value,
"file_path": f.file_path,
"line_number": f.line_number,
"matched_text": f.matched_text,
"recommendation": f.recommendation
"""List all secret patterns."""
print("SECRET DETECTION PATTERNS")
for pattern in sorted(SECRET_PATTERNS, key=lambda p: p.pattern_id):
print(f"\n[{pattern.pattern_id}] {pattern.name}")
print(f" Severity: {pattern.severity.value.upper()}")
print(f" Description: {pattern.description}")
parser = argparse.ArgumentParser(
description="Secret Scanner - Detect hardcoded secrets in code",
formatter_class=argparse.RawDescriptionHelpFormatter,
# Scan a project directory
python secret_scanner.py /path/to/project
python secret_scanner.py /path/to/config.py
python secret_scanner.py /path/to/project --format json
# List all detection patterns
python secret_scanner.py --list-patterns
python secret_scanner.py /path/to/project --output report.txt
help="Path to scan (file or directory)"
choices=["text", "json"],
help="Output format (default: text)"
help="List all detection patterns"
choices=["critical", "high", "medium", "low"],
help="Minimum severity to report"
args = parser.parse_args()
parser.error("path is required (or use --list-patterns)")
print(f"Error: Path does not exist: {path}")
# Filter patterns by severity
patterns = SECRET_PATTERNS
severity_order = ["critical", "high", "medium", "low"]
min_index = severity_order.index(args.severity)
allowed = set(Severity(s) for s in severity_order[:min_index + 1])
patterns = [p for p in patterns if p.severity in allowed]
findings = scan_file(path, patterns)
findings = scan_directory(path, patterns)
if args.format == "json":
output = json.dumps(format_json_report(findings, str(path)), indent=2)
output = format_text_report(findings, str(path))
with open(args.output, 'w') as f:
print(f"Report written to {args.output}")
# Exit code based on findings
if any(f.severity in (Severity.CRITICAL, Severity.HIGH) for f in findings):
if __name__ == "__main__":