284 lines
11 KiB
Python
284 lines
11 KiB
Python
"""
|
|
Unit tests for the pre-commit secret leak scanner.
|
|
|
|
Follows TDD: tests were written before implementation.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
# Add .githooks to path so we can import pre-commit.py as a module
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / ".githooks"))
|
|
|
|
# The module name contains a hyphen, so we import via importlib
|
|
import importlib.util
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"pre_commit_secret_leak",
|
|
str(Path(__file__).resolve().parent.parent / ".githooks" / "pre-commit.py"),
|
|
)
|
|
pre_commit = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(pre_commit)
|
|
|
|
|
|
class TestSecretPatterns(unittest.TestCase):
|
|
"""Tests for individual secret detection patterns."""
|
|
|
|
# ------------------------------------------------------------------
|
|
# API keys
|
|
# ------------------------------------------------------------------
|
|
def test_detects_openai_sk_key(self):
|
|
line = 'api_key = "sk-abcdefghijklmnopqrstuvwxyz1234"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
self.assertIn("sk-", findings[0].message)
|
|
|
|
def test_detects_bearer_token(self):
|
|
line = 'headers = {"Authorization": "Bearer abcdefghijklmnopqrstuvwxyz1234"}'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
self.assertIn("Bearer", findings[0].message)
|
|
|
|
def test_short_bearer_ignored(self):
|
|
line = 'Authorization: Bearer short'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Environment variable assignments
|
|
# ------------------------------------------------------------------
|
|
def test_detects_openai_api_key_assignment(self):
|
|
line = 'OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz1234'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_gitea_token_assignment(self):
|
|
line = 'GITEA_TOKEN=gtl_abcdefghijklmnopqrstuvwxyz1234'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_anthropic_key_assignment(self):
|
|
line = 'ANTHROPIC_API_KEY=sk-ant-abcdefghijklmnopqrstuvwxyz1234'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_kimi_key_assignment(self):
|
|
line = 'KIMI_API_KEY=abcdef1234567890abcdef1234567890'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_telegram_token_assignment(self):
|
|
line = 'TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_discord_token_assignment(self):
|
|
line = 'DISCORD_TOKEN=MzIwNDE5MzA1NjUyNDgzMjY0.DSDsdQ.oM6WmR2i_uIvJhMZZZz0'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Safe env reads / placeholders
|
|
# ------------------------------------------------------------------
|
|
def test_os_environ_get_is_safe(self):
|
|
line = 'key = os.environ.get("OPENAI_API_KEY")'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_placeholder_your_api_key_is_safe(self):
|
|
line = 'OPENAI_API_KEY=<YOUR_API_KEY>'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_placeholder_stars_is_safe(self):
|
|
line = 'OPENAI_API_KEY=***'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_placeholder_redacted_is_safe(self):
|
|
line = 'OPENAI_API_KEY=REDACTED'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_env_var_reference_is_safe(self):
|
|
line = 'OPENAI_API_KEY=$OPENAI_API_KEY'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_empty_env_assignment_is_safe(self):
|
|
line = 'OPENAI_API_KEY='
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Token file paths
|
|
# ------------------------------------------------------------------
|
|
def test_detects_dotenv_path(self):
|
|
line = 'load_dotenv(".env")'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_secrets_json_path(self):
|
|
line = 'with open("secrets.json") as f:'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_keystore_json_path(self):
|
|
line = 'keystore = "/root/nostr-relay/keystore.json"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_hermes_credentials_path(self):
|
|
line = 'creds_path = "~/.hermes/credentials/default.json"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_credentials_json(self):
|
|
line = 'with open("credentials.json") as f:'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_token_json(self):
|
|
line = 'token_file = "token.json"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_api_keys_json(self):
|
|
line = 'keys = "api_keys.json"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Private key blocks
|
|
# ------------------------------------------------------------------
|
|
def test_detects_begin_private_key(self):
|
|
line = '-----BEGIN PRIVATE KEY-----'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_begin_rsa_private_key(self):
|
|
line = '-----BEGIN RSA PRIVATE KEY-----'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
def test_detects_begin_openssh_private_key(self):
|
|
line = '-----BEGIN OPENSSH PRIVATE KEY-----'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Passwords in URLs
|
|
# ------------------------------------------------------------------
|
|
def test_detects_password_in_https_url(self):
|
|
line = 'url = "https://user:secretpassword@example.com/repo.git"'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
self.assertIn("password", findings[0].message.lower())
|
|
|
|
def test_detects_password_in_http_url(self):
|
|
line = 'http://admin:password123@internal.local'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Raw token patterns in strings
|
|
# ------------------------------------------------------------------
|
|
def test_detects_raw_token_in_json(self):
|
|
line = '{"token": "abcdefghijklmnopqrstuvwxyz"}'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
self.assertIn("token", findings[0].message.lower())
|
|
|
|
def test_detects_raw_api_key_in_json(self):
|
|
line = '{"api_key": "1234567890abcdef"}'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertTrue(findings)
|
|
self.assertIn("api_key", findings[0].message.lower())
|
|
|
|
def test_short_token_ignored(self):
|
|
line = '{"token": "short"}'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Documentation / example safe patterns
|
|
# ------------------------------------------------------------------
|
|
def test_documentation_reference_is_safe(self):
|
|
line = 'See the documentation at https://docs.example.com'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
# No specific pattern should match a doc URL without a password
|
|
self.assertFalse(findings)
|
|
|
|
def test_example_code_comment_is_safe(self):
|
|
line = '# Example: OPENAI_API_KEY=<YOUR_API_KEY>'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
def test_doc_string_with_placeholder_is_safe(self):
|
|
line = '"""Set ANTHROPIC_API_KEY to $ANTHROPIC_API_KEY in production."""'
|
|
findings = list(pre_commit.scan_line(line, "test.py", 1))
|
|
self.assertFalse(findings)
|
|
|
|
|
|
class TestScanContent(unittest.TestCase):
|
|
"""Tests for scanning multi-line content."""
|
|
|
|
def test_scan_content_finds_multiple_leaks(self):
|
|
content = """
|
|
OPENAI_API_KEY=sk-12345678901234567890
|
|
Some normal code here
|
|
GITEA_TOKEN=gtl_12345678901234567890
|
|
"""
|
|
findings = pre_commit.scan_content(content, "test.py")
|
|
self.assertEqual(len(findings), 2)
|
|
# Should have line numbers
|
|
self.assertIn(2, [f.line for f in findings])
|
|
self.assertIn(4, [f.line for f in findings])
|
|
|
|
def test_scan_content_returns_empty_when_clean(self):
|
|
content = "print('hello world')\n"
|
|
findings = pre_commit.scan_content(content, "test.py")
|
|
self.assertEqual(findings, [])
|
|
|
|
|
|
class TestScanFiles(unittest.TestCase):
|
|
"""Tests for the file-list scanning entrypoint."""
|
|
|
|
def test_scan_files_skips_binary(self):
|
|
files = ["image.png", "test.py"]
|
|
content_map = {
|
|
"image.png": b"\x89PNG\r\n\x1a\n",
|
|
"test.py": "OPENAI_API_KEY=sk-12345678901234567890\n",
|
|
}
|
|
findings = pre_commit.scan_files(files, lambda f: content_map.get(f, b""))
|
|
self.assertEqual(len(findings), 1)
|
|
self.assertEqual(findings[0].filename, "test.py")
|
|
|
|
def test_scan_files_ignores_safe_lines(self):
|
|
files = ["test.py"]
|
|
content_map = {
|
|
"test.py": "key = os.environ.get('OPENAI_API_KEY')\n",
|
|
}
|
|
findings = pre_commit.scan_files(files, lambda f: content_map.get(f, b""))
|
|
self.assertEqual(findings, [])
|
|
|
|
|
|
class TestCliHelpers(unittest.TestCase):
|
|
"""Tests for CLI helper functions."""
|
|
|
|
def test_color_codes_present(self):
|
|
self.assertIn("\033[", pre_commit.RED)
|
|
self.assertIn("\033[", pre_commit.GREEN)
|
|
|
|
def test_is_binary_content_true(self):
|
|
self.assertTrue(pre_commit.is_binary_content(b"\x00\x01\x02"))
|
|
|
|
def test_is_binary_content_false(self):
|
|
self.assertFalse(pre_commit.is_binary_content(b"hello world\n"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|