security: add pre-commit hook for secret leak detection (#384)
This commit is contained in:
42
.pre-commit-hooks.yaml
Normal file
42
.pre-commit-hooks.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Pre-commit hooks configuration for timmy-home
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
|
||||||
|
repos:
|
||||||
|
# Standard pre-commit hooks
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude: '\.(md|txt)$'
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: '\.(md|txt)$'
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-json
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: ['--maxkb=5000']
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-symlinks
|
||||||
|
- id: detect-private-key
|
||||||
|
|
||||||
|
# Secret detection - custom local hook
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: detect-secrets
|
||||||
|
name: Detect Secrets
|
||||||
|
description: Scan for API keys, tokens, and other secrets
|
||||||
|
entry: python3 scripts/detect_secrets.py
|
||||||
|
language: python
|
||||||
|
types: [text]
|
||||||
|
exclude:
|
||||||
|
'(?x)^(
|
||||||
|
.*\.md$|
|
||||||
|
.*\.svg$|
|
||||||
|
.*\.lock$|
|
||||||
|
.*-lock\..*$|
|
||||||
|
\.gitignore$|
|
||||||
|
\.secrets\.baseline$|
|
||||||
|
tests/test_secret_detection\.py$
|
||||||
|
)'
|
||||||
|
pass_filenames: true
|
||||||
|
require_serial: false
|
||||||
|
verbose: true
|
||||||
132
README.md
Normal file
132
README.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Timmy Home
|
||||||
|
|
||||||
|
Timmy Foundation's home repository for development operations and configurations.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Pre-commit Hook for Secret Detection
|
||||||
|
|
||||||
|
This repository includes a pre-commit hook that automatically scans for secrets (API keys, tokens, passwords) before allowing commits.
|
||||||
|
|
||||||
|
#### Setup
|
||||||
|
|
||||||
|
Install pre-commit hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### What Gets Scanned
|
||||||
|
|
||||||
|
The hook detects:
|
||||||
|
- **API Keys**: OpenAI (`sk-*`), Anthropic (`sk-ant-*`), AWS, Stripe
|
||||||
|
- **Private Keys**: RSA, DSA, EC, OpenSSH private keys
|
||||||
|
- **Tokens**: GitHub (`ghp_*`), Gitea, Slack, Telegram, JWT, Bearer tokens
|
||||||
|
- **Database URLs**: Connection strings with embedded credentials
|
||||||
|
- **Passwords**: Hardcoded passwords in configuration files
|
||||||
|
|
||||||
|
#### How It Works
|
||||||
|
|
||||||
|
Before each commit, the hook:
|
||||||
|
1. Scans all staged text files
|
||||||
|
2. Checks against patterns for common secret formats
|
||||||
|
3. Reports any potential secrets found
|
||||||
|
4. Blocks the commit if secrets are detected
|
||||||
|
|
||||||
|
#### Handling False Positives
|
||||||
|
|
||||||
|
If the hook flags something that is not actually a secret (e.g., test fixtures, placeholder values), you can:
|
||||||
|
|
||||||
|
**Option 1: Add an exclusion marker to the line**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add one of these markers to the end of the line:
|
||||||
|
api_key = "sk-test123" # pragma: allowlist secret
|
||||||
|
api_key = "sk-test123" # noqa: secret
|
||||||
|
api_key = "sk-test123" # secret-detection:ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Use placeholder values (auto-excluded)**
|
||||||
|
|
||||||
|
These patterns are automatically excluded:
|
||||||
|
- `changeme`, `password`, `123456`, `admin` (common defaults)
|
||||||
|
- Values containing `fake_`, `test_`, `dummy_`, `example_`, `placeholder_`
|
||||||
|
- URLs with `localhost` or `127.0.0.1`
|
||||||
|
|
||||||
|
**Option 3: Skip the hook (emergency only)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit --no-verify # Bypasses all pre-commit hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Warning**: Only use `--no-verify` if you are certain no real secrets are being committed.
|
||||||
|
|
||||||
|
#### CI/CD Integration
|
||||||
|
|
||||||
|
The secret detection script can also be run in CI/CD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scan specific files
|
||||||
|
python3 scripts/detect_secrets.py file1.py file2.yaml
|
||||||
|
|
||||||
|
# Scan with verbose output
|
||||||
|
python3 scripts/detect_secrets.py --verbose src/
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
python3 tests/test_secret_detection.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Excluded Files
|
||||||
|
|
||||||
|
The following are automatically excluded from scanning:
|
||||||
|
- Markdown files (`.md`)
|
||||||
|
- Lock files (`package-lock.json`, `poetry.lock`, `yarn.lock`)
|
||||||
|
- Image and font files
|
||||||
|
- `node_modules/`, `__pycache__/`, `.git/`
|
||||||
|
|
||||||
|
#### Testing the Detection
|
||||||
|
|
||||||
|
To verify the detection works:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the test suite
|
||||||
|
python3 tests/test_secret_detection.py
|
||||||
|
|
||||||
|
# Test with a specific file
|
||||||
|
echo "API_KEY=sk-test123456789" > /tmp/test_secret.py
|
||||||
|
python3 scripts/detect_secrets.py /tmp/test_secret.py
|
||||||
|
# Should report: OpenAI API key detected
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run secret detection tests
|
||||||
|
python3 tests/test_secret_detection.py
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── .pre-commit-hooks.yaml # Pre-commit configuration
|
||||||
|
├── scripts/
|
||||||
|
│ └── detect_secrets.py # Secret detection script
|
||||||
|
├── tests/
|
||||||
|
│ └── test_secret_detection.py # Test cases
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is part of the Timmy Foundation.
|
||||||
323
scripts/detect_secrets.py
Executable file
323
scripts/detect_secrets.py
Executable file
@@ -0,0 +1,323 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Secret leak detection script for pre-commit hooks.
|
||||||
|
|
||||||
|
Detects common secret patterns in staged files:
|
||||||
|
- API keys (sk-*, pk_*, etc.)
|
||||||
|
- Private keys (-----BEGIN PRIVATE KEY-----)
|
||||||
|
- Passwords in config files
|
||||||
|
- GitHub/Gitea tokens
|
||||||
|
- Database connection strings with credentials
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
# Secret patterns to detect
|
||||||
|
SECRET_PATTERNS = {
|
||||||
|
"openai_api_key": {
|
||||||
|
"pattern": r"sk-[a-zA-Z0-9]{20,}",
|
||||||
|
"description": "OpenAI API key",
|
||||||
|
},
|
||||||
|
"anthropic_api_key": {
|
||||||
|
"pattern": r"sk-ant-[a-zA-Z0-9]{32,}",
|
||||||
|
"description": "Anthropic API key",
|
||||||
|
},
|
||||||
|
"generic_api_key": {
|
||||||
|
"pattern": r"(?i)(api[_-]?key|apikey)\s*[:=]\s*['\"]?([a-zA-Z0-9_\-]{16,})['\"]?",
|
||||||
|
"description": "Generic API key",
|
||||||
|
},
|
||||||
|
"private_key": {
|
||||||
|
"pattern": r"-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----",
|
||||||
|
"description": "Private key",
|
||||||
|
},
|
||||||
|
"github_token": {
|
||||||
|
"pattern": r"gh[pousr]_[A-Za-z0-9_]{36,}",
|
||||||
|
"description": "GitHub token",
|
||||||
|
},
|
||||||
|
"gitea_token": {
|
||||||
|
"pattern": r"gitea_[a-f0-9]{40}",
|
||||||
|
"description": "Gitea token",
|
||||||
|
},
|
||||||
|
"aws_access_key": {
|
||||||
|
"pattern": r"AKIA[0-9A-Z]{16}",
|
||||||
|
"description": "AWS Access Key ID",
|
||||||
|
},
|
||||||
|
"aws_secret_key": {
|
||||||
|
"pattern": r"(?i)aws[_-]?secret[_-]?(access)?[_-]?key\s*[:=]\s*['\"]?([a-zA-Z0-9/+=]{40})['\"]?",
|
||||||
|
"description": "AWS Secret Access Key",
|
||||||
|
},
|
||||||
|
"database_connection_string": {
|
||||||
|
"pattern": r"(?i)(mongodb|mysql|postgresql|postgres|redis)://[^:]+:[^@]+@[^/]+",
|
||||||
|
"description": "Database connection string with credentials",
|
||||||
|
},
|
||||||
|
"password_in_config": {
|
||||||
|
"pattern": r"(?i)(password|passwd|pwd)\s*[:=]\s*['\"]([^'\"]{4,})['\"]",
|
||||||
|
"description": "Hardcoded password",
|
||||||
|
},
|
||||||
|
"stripe_key": {
|
||||||
|
"pattern": r"sk_(live|test)_[0-9a-zA-Z]{24,}",
|
||||||
|
"description": "Stripe API key",
|
||||||
|
},
|
||||||
|
"slack_token": {
|
||||||
|
"pattern": r"xox[baprs]-[0-9a-zA-Z]{10,}",
|
||||||
|
"description": "Slack token",
|
||||||
|
},
|
||||||
|
"telegram_bot_token": {
|
||||||
|
"pattern": r"[0-9]{8,10}:[a-zA-Z0-9_-]{35}",
|
||||||
|
"description": "Telegram bot token",
|
||||||
|
},
|
||||||
|
"jwt_token": {
|
||||||
|
"pattern": r"eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*",
|
||||||
|
"description": "JWT token",
|
||||||
|
},
|
||||||
|
"bearer_token": {
|
||||||
|
"pattern": r"(?i)bearer\s+[a-zA-Z0-9_\-\.=]{20,}",
|
||||||
|
"description": "Bearer token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Files/patterns to exclude from scanning
|
||||||
|
EXCLUSIONS = {
|
||||||
|
"files": {
|
||||||
|
".pre-commit-hooks.yaml",
|
||||||
|
".gitignore",
|
||||||
|
"poetry.lock",
|
||||||
|
"package-lock.json",
|
||||||
|
"yarn.lock",
|
||||||
|
"Pipfile.lock",
|
||||||
|
".secrets.baseline",
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
".md",
|
||||||
|
".svg",
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".gif",
|
||||||
|
".ico",
|
||||||
|
".woff",
|
||||||
|
".woff2",
|
||||||
|
".ttf",
|
||||||
|
".eot",
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
".git/",
|
||||||
|
"node_modules/",
|
||||||
|
"__pycache__/",
|
||||||
|
".pytest_cache/",
|
||||||
|
".mypy_cache/",
|
||||||
|
".venv/",
|
||||||
|
"venv/",
|
||||||
|
".tox/",
|
||||||
|
"dist/",
|
||||||
|
"build/",
|
||||||
|
".eggs/",
|
||||||
|
},
|
||||||
|
"patterns": {
|
||||||
|
r"your_[a-z_]+_here",
|
||||||
|
r"example_[a-z_]+",
|
||||||
|
r"dummy_[a-z_]+",
|
||||||
|
r"test_[a-z_]+",
|
||||||
|
r"fake_[a-z_]+",
|
||||||
|
r"password\s*[=:]\s*['\"]?(changeme|password|123456|admin)['\"]?",
|
||||||
|
r"#.*(?:example|placeholder|sample)",
|
||||||
|
r"(mongodb|mysql|postgresql)://[^:]+:[^@]+@localhost",
|
||||||
|
r"(mongodb|mysql|postgresql)://[^:]+:[^@]+@127\.0\.0\.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Markers for inline exclusions
|
||||||
|
EXCLUSION_MARKERS = [
|
||||||
|
"# pragma: allowlist secret",
|
||||||
|
"# noqa: secret",
|
||||||
|
"// pragma: allowlist secret",
|
||||||
|
"/* pragma: allowlist secret */",
|
||||||
|
"# secret-detection:ignore",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def should_exclude_file(file_path: str) -> bool:
|
||||||
|
"""Check if file should be excluded from scanning."""
|
||||||
|
path = Path(file_path)
|
||||||
|
|
||||||
|
if path.name in EXCLUSIONS["files"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if path.suffix.lower() in EXCLUSIONS["extensions"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for excluded_path in EXCLUSIONS["paths"]:
|
||||||
|
if excluded_path in str(path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_exclusion_marker(line: str) -> bool:
|
||||||
|
"""Check if line has an exclusion marker."""
|
||||||
|
return any(marker in line for marker in EXCLUSION_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
|
def is_excluded_match(line: str, match_str: str) -> bool:
|
||||||
|
"""Check if the match should be excluded."""
|
||||||
|
for pattern in EXCLUSIONS["patterns"]:
|
||||||
|
if re.search(pattern, line, re.IGNORECASE):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if re.search(r"['\"](fake|test|dummy|example|placeholder|changeme)['\"]", line, re.IGNORECASE):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def scan_file(file_path: str) -> List[Tuple[int, str, str, str]]:
|
||||||
|
"""Scan a single file for secrets.
|
||||||
|
|
||||||
|
Returns list of tuples: (line_number, line_content, pattern_name, description)
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
print(f"Warning: Could not read {file_path}: {e}", file=sys.stderr)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
for line_num, line in enumerate(lines, 1):
|
||||||
|
if has_exclusion_marker(line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for pattern_name, pattern_info in SECRET_PATTERNS.items():
|
||||||
|
matches = re.finditer(pattern_info["pattern"], line)
|
||||||
|
for match in matches:
|
||||||
|
match_str = match.group(0)
|
||||||
|
|
||||||
|
if is_excluded_match(line, match_str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
findings.append(
|
||||||
|
(line_num, line.strip(), pattern_name, pattern_info["description"])
|
||||||
|
)
|
||||||
|
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def scan_files(file_paths: List[str]) -> dict:
|
||||||
|
"""Scan multiple files for secrets.
|
||||||
|
|
||||||
|
Returns dict: {file_path: [(line_num, line, pattern, description), ...]}
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for file_path in file_paths:
|
||||||
|
if should_exclude_file(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
if findings:
|
||||||
|
results[file_path] = findings
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def print_findings(results: dict) -> None:
|
||||||
|
"""Print secret findings in a readable format."""
|
||||||
|
if not results:
|
||||||
|
return
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("POTENTIAL SECRETS DETECTED!")
|
||||||
|
print("=" * 80)
|
||||||
|
print()
|
||||||
|
|
||||||
|
total_findings = 0
|
||||||
|
for file_path, findings in results.items():
|
||||||
|
print(f"\nFILE: {file_path}")
|
||||||
|
print("-" * 40)
|
||||||
|
for line_num, line, pattern_name, description in findings:
|
||||||
|
total_findings += 1
|
||||||
|
print(f" Line {line_num}: {description}")
|
||||||
|
print(f" Pattern: {pattern_name}")
|
||||||
|
print(f" Content: {line[:100]}{'...' if len(line) > 100 else ''}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Total findings: {total_findings}")
|
||||||
|
print("=" * 80)
|
||||||
|
print()
|
||||||
|
print("To fix this:")
|
||||||
|
print(" 1. Remove the secret from the file")
|
||||||
|
print(" 2. Use environment variables or a secrets manager")
|
||||||
|
print(" 3. If this is a false positive, add an exclusion marker:")
|
||||||
|
print(" - Add '# pragma: allowlist secret' to the end of the line")
|
||||||
|
print(" - Or add '# secret-detection:ignore' to the end of the line")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Detect secrets in files",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
%(prog)s file1.py file2.yaml
|
||||||
|
%(prog)s --exclude "*.md" src/
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - No secrets found
|
||||||
|
1 - Secrets detected
|
||||||
|
2 - Error
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"files",
|
||||||
|
nargs="+",
|
||||||
|
help="Files to scan",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exclude",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Additional file patterns to exclude",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
action="store_true",
|
||||||
|
help="Print verbose output",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
files_to_scan = []
|
||||||
|
for file_path in args.files:
|
||||||
|
if should_exclude_file(file_path):
|
||||||
|
if args.verbose:
|
||||||
|
print(f"Skipping excluded file: {file_path}")
|
||||||
|
continue
|
||||||
|
files_to_scan.append(file_path)
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
print(f"Scanning {len(files_to_scan)} files...")
|
||||||
|
|
||||||
|
results = scan_files(files_to_scan)
|
||||||
|
|
||||||
|
if results:
|
||||||
|
print_findings(results)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
print("No secrets detected!")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
106
tests/test_secret_detection.py
Normal file
106
tests/test_secret_detection.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test cases for secret detection script.
|
||||||
|
|
||||||
|
These tests verify that the detect_secrets.py script correctly:
|
||||||
|
1. Detects actual secrets
|
||||||
|
2. Ignores false positives
|
||||||
|
3. Respects exclusion markers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add scripts directory to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||||||
|
|
||||||
|
from detect_secrets import (
|
||||||
|
scan_file,
|
||||||
|
scan_files,
|
||||||
|
should_exclude_file,
|
||||||
|
has_exclusion_marker,
|
||||||
|
is_excluded_match,
|
||||||
|
SECRET_PATTERNS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSecretDetection(unittest.TestCase):
|
||||||
|
"""Test cases for secret detection."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.test_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up test fixtures."""
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def _create_test_file(self, content: str, filename: str = "test.txt") -> str:
|
||||||
|
"""Create a test file with given content."""
|
||||||
|
file_path = os.path.join(self.test_dir, filename)
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def test_detect_openai_api_key(self):
|
||||||
|
"""Test detection of OpenAI API keys."""
|
||||||
|
content = "api_key = 'sk-abcdefghijklmnopqrstuvwxyz123456'"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertTrue(any("openai" in f[2].lower() for f in findings))
|
||||||
|
|
||||||
|
def test_detect_private_key(self):
|
||||||
|
"""Test detection of private keys."""
|
||||||
|
content = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MhgwMbRvI0MBZhpF\n-----END RSA PRIVATE KEY-----"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertTrue(any("private" in f[2].lower() for f in findings))
|
||||||
|
|
||||||
|
def test_detect_database_connection_string(self):
|
||||||
|
"""Test detection of database connection strings with credentials."""
|
||||||
|
content = "DATABASE_URL=mongodb://admin:secretpassword@mongodb.example.com:27017/db"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertTrue(any("database" in f[2].lower() for f in findings))
|
||||||
|
|
||||||
|
def test_detect_password_in_config(self):
|
||||||
|
"""Test detection of hardcoded passwords."""
|
||||||
|
content = "password = 'mysecretpassword123'"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertTrue(any("password" in f[2].lower() for f in findings))
|
||||||
|
|
||||||
|
def test_exclude_placeholder_passwords(self):
|
||||||
|
"""Test that placeholder passwords are excluded."""
|
||||||
|
content = "password = 'changeme'"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertEqual(len(findings), 0)
|
||||||
|
|
||||||
|
def test_exclude_localhost_database_url(self):
|
||||||
|
"""Test that localhost database URLs are excluded."""
|
||||||
|
content = "DATABASE_URL=mongodb://admin:secret@localhost:27017/db"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertEqual(len(findings), 0)
|
||||||
|
|
||||||
|
def test_pragma_allowlist_secret(self):
|
||||||
|
"""Test '# pragma: allowlist secret' marker."""
|
||||||
|
content = "api_key = 'sk-abcdefghijklmnopqrstuvwxyz123456' # pragma: allowlist secret"
|
||||||
|
file_path = self._create_test_file(content)
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertEqual(len(findings), 0)
|
||||||
|
|
||||||
|
def test_empty_file(self):
|
||||||
|
"""Test scanning empty file."""
|
||||||
|
file_path = self._create_test_file("")
|
||||||
|
findings = scan_file(file_path)
|
||||||
|
self.assertEqual(len(findings), 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user