test_security_secrets.py•35.7 kB
"""Comprehensive tests for security.secrets module."""
import io
import sys
import tempfile
from pathlib import Path
import pytest
from mnemex.security.secrets import (
SecretMatch,
detect_secrets,
format_secret_warning,
main,
redact_secrets,
scan_file_for_secrets,
should_warn_about_secrets,
)
class TestSecretMatch:
"""Tests for SecretMatch dataclass."""
def test_create_secret_match_instance(self):
"""Test creating a SecretMatch instance."""
match = SecretMatch(
secret_type="api_key",
position=10,
context="API key: sk-...",
)
assert match.secret_type == "api_key"
assert match.position == 10
assert match.context == "API key: sk-..."
def test_secret_match_attributes(self):
"""Test SecretMatch has correct attributes."""
match = SecretMatch(
secret_type="aws_access_key",
position=42,
context="AWS key: AKIA...",
)
assert hasattr(match, "secret_type")
assert hasattr(match, "position")
assert hasattr(match, "context")
def test_secret_match_with_different_types(self):
"""Test creating SecretMatch with various secret types."""
types = ["openai_key", "github_token", "database_url", "jwt_token"]
for secret_type in types:
match = SecretMatch(
secret_type=secret_type,
position=0,
context="test context",
)
assert match.secret_type == secret_type
class TestDetectSecretsAPIKeys:
"""Tests for detect_secrets function - API keys."""
def test_detect_generic_api_key_uppercase(self):
"""Test detecting generic API_KEY pattern."""
text = "API_KEY=abcd1234efgh5678ijkl9012mnop3456qrst"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "api_key" for m in matches)
def test_detect_generic_api_key_lowercase(self):
"""Test detecting lowercase api_key pattern."""
text = "api_key: abcd1234efgh5678ijkl9012mnop3456qrst"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "api_key" for m in matches)
def test_detect_api_token(self):
"""Test detecting API_TOKEN pattern."""
text = "API_TOKEN = xyz123abc456def789ghi012jkl345mno"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "api_key" for m in matches)
def test_detect_access_key(self):
"""Test detecting ACCESS_KEY pattern."""
text = 'ACCESS_KEY="test1234567890abcdefghij"'
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "api_key" for m in matches)
class TestDetectSecretsAWS:
"""Tests for detect_secrets function - AWS credentials."""
def test_detect_aws_access_key_id(self):
"""Test detecting AWS access key (AKIA...)."""
text = "AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "aws_access_key" for m in matches)
def test_detect_aws_access_key_in_sentence(self):
"""Test detecting AWS access key embedded in text."""
text = "My access key is AKIAI44QH8DHBEXAMPLE and it's secret"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "aws_access_key" for m in matches)
def test_detect_aws_secret_access_key(self):
"""Test detecting AWS secret access key."""
text = "aws_secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "aws_secret_key" for m in matches)
def test_detect_aws_secret_with_equals(self):
"""Test detecting AWS secret with equals sign."""
text = "AWS_SECRET_ACCESS_KEY=abcd1234efgh5678ijkl9012mnop3456qrst7890"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "aws_secret_key" for m in matches)
class TestDetectSecretsGitHub:
"""Tests for detect_secrets function - GitHub tokens."""
def test_detect_github_personal_access_token(self):
"""Test detecting GitHub personal access token (ghp_)."""
text = "GITHUB_TOKEN=ghp_1234567890abcdefghijklmnopqrstuvwxyzABCDEF"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type in ("github_token", "github_classic") for m in matches)
def test_detect_github_oauth_token(self):
"""Test detecting GitHub OAuth token (gho_)."""
text = "token: gho_abcdefghijklmnopqrstuvwxyz1234567890ABCDEF"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "github_token" for m in matches)
def test_detect_github_user_to_server_token(self):
"""Test detecting GitHub user-to-server token (ghu_)."""
text = "Authorization: ghu_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "github_token" for m in matches)
def test_detect_github_server_to_server_token(self):
"""Test detecting GitHub server-to-server token (ghs_)."""
text = "GH_TOKEN=ghs_abcdefghijklmnopqrstuvwxyz1234567890ABCD"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "github_token" for m in matches)
def test_detect_github_refresh_token(self):
"""Test detecting GitHub refresh token (ghr_)."""
text = "refresh_token=ghr_1234567890abcdefghijklmnopqrstuvwxyzABCDEF"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "github_token" for m in matches)
class TestDetectSecretsOpenAI:
"""Tests for detect_secrets function - OpenAI keys."""
def test_detect_openai_api_key(self):
"""Test detecting OpenAI API key (sk-)."""
text = "OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "openai_key" for m in matches)
def test_detect_openai_key_in_code(self):
"""Test detecting OpenAI key in code snippet."""
text = 'client = OpenAI(api_key="sk-abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNO")'
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "openai_key" for m in matches)
class TestDetectSecretsAnthropic:
"""Tests for detect_secrets function - Anthropic keys."""
def test_detect_anthropic_api_key(self):
"""Test detecting Anthropic API key (sk-ant-)."""
text = "ANTHROPIC_API_KEY=sk-ant-api03-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqr"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "anthropic_key" for m in matches)
def test_detect_anthropic_key_with_hyphens(self):
"""Test detecting Anthropic key with hyphens."""
text = "api_key: sk-ant-api03-abcd-efgh-ijkl-mnop-qrst-uvwx-yzAB-CDEF-GHIJ-KLMN-OPQR-STUV-WXYZ-1234-5678-90ab-cdef-ghij-klmn"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "anthropic_key" for m in matches)
class TestDetectSecretsGoogle:
"""Tests for detect_secrets function - Google API keys."""
def test_detect_google_api_key(self):
"""Test detecting Google API key (AIza...)."""
text = "GOOGLE_API_KEY=AIzaSyD1234567890abcdefghijklmnopqrstuv"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "google_api_key" for m in matches)
def test_detect_google_key_in_url(self):
"""Test detecting Google API key in URL."""
text = "https://maps.googleapis.com/maps/api/geocode/json?key=AIzaSyAbCdEfGhIjKlMnOpQrStUvWxYz1234567&address=test"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "google_api_key" for m in matches)
class TestDetectSecretsSlack:
"""Tests for detect_secrets function - Slack tokens."""
def test_detect_slack_bot_token(self):
"""Test detecting Slack bot token (xoxb-)."""
text = "SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-abcdefghijklmnopqrstuvwx"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "slack_token" for m in matches)
def test_detect_slack_user_token(self):
"""Test detecting Slack user token (xoxp-)."""
text = "token: xoxp-1234567890-1234567890-abcdefghijklmnopqrstuvwx"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "slack_token" for m in matches)
def test_detect_slack_app_token(self):
"""Test detecting Slack app token (xoxa-)."""
text = "SLACK_APP_TOKEN=xoxa-1234567890-1234567890123-abcdefghijklmnopqrstuvwx"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "slack_token" for m in matches)
class TestDetectSecretsBearerTokens:
"""Tests for detect_secrets function - Bearer tokens."""
def test_detect_bearer_token_uppercase(self):
"""Test detecting Bearer token (uppercase)."""
text = "Authorization: Bearer abcdef1234567890ghijkl"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "bearer_token" for m in matches)
def test_detect_bearer_token_lowercase(self):
"""Test detecting bearer token (lowercase)."""
text = "auth: bearer xyz123456789012345678901234567890"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "bearer_token" for m in matches)
def test_detect_bearer_token_with_underscores_hyphens(self):
"""Test detecting bearer token with underscores and hyphens."""
text = "Bearer abc-def_123-456_789-012_345-678_901-234"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "bearer_token" for m in matches)
class TestDetectSecretsJWT:
"""Tests for detect_secrets function - JWT tokens."""
def test_detect_jwt_token(self):
"""Test detecting JWT token."""
text = "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "jwt_token" for m in matches)
def test_detect_jwt_in_header(self):
"""Test detecting JWT in Authorization header."""
text = "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyJ9.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "jwt_token" for m in matches)
class TestDetectSecretsPrivateKeys:
"""Tests for detect_secrets function - Private keys."""
def test_detect_rsa_private_key(self):
"""Test detecting RSA private key."""
text = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1234567890abcdefghijklmnop
-----END RSA PRIVATE KEY-----"""
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "private_key" for m in matches)
def test_detect_private_key_generic(self):
"""Test detecting generic private key."""
text = """-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC
-----END PRIVATE KEY-----"""
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "private_key" for m in matches)
def test_detect_private_key_lowercase(self):
"""Test detecting private key with lowercase."""
text = "-----begin private key-----\ndata\n-----end private key-----"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "private_key" for m in matches)
class TestDetectSecretsDatabaseURLs:
"""Tests for detect_secrets function - Database URLs."""
def test_detect_postgres_url(self):
"""Test detecting PostgreSQL connection string."""
text = "DATABASE_URL=postgres://user:password123@localhost:5432/mydb"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "database_url" for m in matches)
def test_detect_mysql_url(self):
"""Test detecting MySQL connection string."""
text = "DB_URL: mysql://admin:secretpass@db.example.com:3306/production"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "database_url" for m in matches)
def test_detect_mongodb_url(self):
"""Test detecting MongoDB connection string."""
text = "MONGO_URI=mongodb://dbuser:dbpass123@mongo.example.com:27017/myapp"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "database_url" for m in matches)
def test_detect_redis_url(self):
"""Test detecting Redis connection string."""
text = "REDIS_URL=redis://default:redispass@redis.local:6379/0"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "database_url" for m in matches)
def test_detect_postgres_scheme(self):
"""Test detecting postgres:// scheme (not postgresql)."""
text = "postgres://user:pass@host/db"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "database_url" for m in matches)
class TestDetectSecretsPasswords:
"""Tests for detect_secrets function - Password assignments."""
def test_detect_password_equals(self):
"""Test detecting password with equals sign."""
text = "password=MySecretPass123!"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "password_assignment" for m in matches)
def test_detect_passwd_colon(self):
"""Test detecting passwd with colon."""
text = "passwd: SuperSecret456"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "password_assignment" for m in matches)
def test_detect_pwd_quoted(self):
"""Test detecting pwd in quotes."""
text = 'PWD="MyPassword789"'
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "password_assignment" for m in matches)
def test_detect_password_case_insensitive(self):
"""Test detecting PASSWORD in uppercase."""
text = "PASSWORD = AdminPass2023"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "password_assignment" for m in matches)
class TestDetectSecretsSecretAssignments:
"""Tests for detect_secrets function - Secret assignments."""
def test_detect_secret_assignment(self):
"""Test detecting secret assignment."""
text = "secret=abc123def456ghi789jkl012mno"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "secret_assignment" for m in matches)
def test_detect_token_assignment(self):
"""Test detecting token assignment."""
text = "token: xyz987wvu654tsr321qpo098nml"
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type in ("secret_assignment", "api_key") for m in matches)
def test_detect_credential_assignment(self):
"""Test detecting credential assignment."""
text = 'credential="abcdefghijklmnopqrstuvwxyz123456"'
matches = detect_secrets(text)
assert len(matches) >= 1
assert any(m.secret_type == "secret_assignment" for m in matches)
class TestDetectSecretsMultipleAndEdgeCases:
"""Tests for detect_secrets function - Multiple secrets and edge cases."""
def test_multiple_secrets_in_same_text(self):
"""Test detecting multiple different secrets in same text."""
text = """
API_KEY=abcd1234efgh5678ijkl9012mnop
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN
password=MySecretPass123
"""
matches = detect_secrets(text)
assert len(matches) >= 4
secret_types = {m.secret_type for m in matches}
assert "api_key" in secret_types or "secret_assignment" in secret_types
assert "aws_access_key" in secret_types
assert "openai_key" in secret_types
assert "password_assignment" in secret_types
def test_text_with_no_secrets(self):
"""Test text containing no secrets."""
text = "This is just normal text without any secrets. Hello world!"
matches = detect_secrets(text)
assert len(matches) == 0
def test_empty_text(self):
"""Test empty text input."""
text = ""
matches = detect_secrets(text)
assert len(matches) == 0
def test_max_matches_parameter(self):
"""Test max_matches parameter limits results."""
text = "\n".join([f"api_key_{i}=abcd1234efgh5678ijkl9012mnop{i:04d}" for i in range(20)])
matches = detect_secrets(text, max_matches=5)
assert len(matches) <= 5
def test_context_chars_parameter(self):
"""Test context_chars parameter affects context length."""
text = "x" * 100 + "API_KEY=abcd1234efgh5678ijkl9012mnop" + "y" * 100
matches_small = detect_secrets(text, context_chars=10)
matches_large = detect_secrets(text, context_chars=50)
assert len(matches_small) >= 1
assert len(matches_large) >= 1
assert len(matches_large[0].context) > len(matches_small[0].context)
def test_secret_position_tracking(self):
"""Test that position is correctly tracked."""
text = "prefix text API_KEY=abcd1234efgh5678ijkl9012mnop suffix"
matches = detect_secrets(text)
assert len(matches) >= 1
assert matches[0].position >= 0
assert matches[0].position < len(text)
def test_context_includes_redaction(self):
"""Test that context includes redacted secret."""
text = "API_KEY=abcd1234efgh5678ijkl9012mnop3456qrst"
matches = detect_secrets(text)
assert len(matches) >= 1
assert "..." in matches[0].context
def test_short_secret_redaction(self):
"""Test redaction of short secrets."""
text = "secret=abcd1234efgh5678ijkl9012"
matches = detect_secrets(text)
assert len(matches) >= 1
assert "***" in matches[0].context or "..." in matches[0].context
class TestScanFileForSecrets:
"""Tests for scan_file_for_secrets function."""
def test_scan_file_with_secrets(self):
"""Test scanning a file containing secrets."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "config.txt"
test_file.write_text("API_KEY=abcd1234efgh5678ijkl9012mnop\nOTHER=value")
matches = scan_file_for_secrets(str(test_file))
assert len(matches) >= 1
assert any(m.secret_type == "api_key" for m in matches)
def test_scan_file_without_secrets(self):
"""Test scanning a file with no secrets."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "normal.txt"
test_file.write_text("This is normal content\nNo secrets here")
matches = scan_file_for_secrets(str(test_file))
assert len(matches) == 0
def test_scan_nonexistent_file_raises_error(self):
"""Test scanning non-existent file raises FileNotFoundError."""
with pytest.raises(FileNotFoundError, match="File not found"):
scan_file_for_secrets("/nonexistent/path/file.txt")
def test_scan_file_max_matches(self):
"""Test max_matches parameter in file scanning."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "many_secrets.txt"
content = "\n".join(
[f"api_key_{i}=abcd1234efgh5678ijkl9012mnop{i:04d}" for i in range(20)]
)
test_file.write_text(content)
matches = scan_file_for_secrets(str(test_file), max_matches=3)
assert len(matches) <= 3
def test_scan_file_with_encoding_errors(self):
"""Test scanning file with encoding errors is handled."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "binary.txt"
test_file.write_bytes(b"API_KEY=test1234567890abcdef\xff\xfe")
matches = scan_file_for_secrets(str(test_file))
assert isinstance(matches, list)
class TestFormatSecretWarning:
"""Tests for format_secret_warning function."""
def test_format_warning_with_single_secret(self):
"""Test formatting warning with one secret."""
matches = [SecretMatch(secret_type="api_key", position=10, context="key=abc...xyz")]
warning = format_secret_warning(matches)
assert "WARNING" in warning
assert "1 potential secret" in warning
assert "api_key" in warning
def test_format_warning_with_multiple_secrets(self):
"""Test formatting warning with multiple secrets."""
matches = [
SecretMatch(secret_type="api_key", position=10, context="key1"),
SecretMatch(secret_type="aws_access_key", position=50, context="key2"),
SecretMatch(secret_type="openai_key", position=100, context="key3"),
]
warning = format_secret_warning(matches)
assert "WARNING" in warning
assert "3 potential secrets" in warning
assert "api_key" in warning
assert "aws_access_key" in warning
assert "openai_key" in warning
def test_format_warning_with_no_secrets(self):
"""Test formatting warning with empty list."""
matches = []
warning = format_secret_warning(matches)
assert warning == ""
def test_format_warning_groups_by_type(self):
"""Test warning groups secrets by type."""
matches = [
SecretMatch(secret_type="api_key", position=10, context="key1"),
SecretMatch(secret_type="api_key", position=20, context="key2"),
SecretMatch(secret_type="aws_access_key", position=30, context="key3"),
]
warning = format_secret_warning(matches)
assert "api_key: 2" in warning
assert "aws_access_key: 1" in warning
def test_format_warning_includes_recommendations(self):
"""Test warning includes security recommendations."""
matches = [SecretMatch(secret_type="api_key", position=10, context="key")]
warning = format_secret_warning(matches)
assert "environment variables" in warning
assert "secrets manager" in warning
assert "MNEMEX_DETECT_SECRETS" in warning
class TestShouldWarnAboutSecrets:
"""Tests for should_warn_about_secrets function."""
def test_warn_for_high_confidence_aws_key(self):
"""Test warning for high-confidence AWS key."""
matches = [SecretMatch(secret_type="aws_access_key", position=10, context="key")]
assert should_warn_about_secrets(matches) is True
def test_warn_for_high_confidence_openai_key(self):
"""Test warning for high-confidence OpenAI key."""
matches = [SecretMatch(secret_type="openai_key", position=10, context="key")]
assert should_warn_about_secrets(matches) is True
def test_warn_for_high_confidence_github_token(self):
"""Test warning for high-confidence GitHub token."""
matches = [SecretMatch(secret_type="github_token", position=10, context="key")]
assert should_warn_about_secrets(matches) is True
def test_warn_for_private_key(self):
"""Test warning for private key."""
matches = [SecretMatch(secret_type="private_key", position=10, context="key")]
assert should_warn_about_secrets(matches) is True
def test_warn_for_database_url(self):
"""Test warning for database URL."""
matches = [SecretMatch(secret_type="database_url", position=10, context="url")]
assert should_warn_about_secrets(matches) is True
def test_no_warn_for_single_low_confidence_match(self):
"""Test no warning for single low-confidence match."""
matches = [SecretMatch(secret_type="api_key", position=10, context="key")]
assert should_warn_about_secrets(matches) is False
def test_warn_for_multiple_low_confidence_matches(self):
"""Test warning for multiple low-confidence matches."""
matches = [
SecretMatch(secret_type="api_key", position=10, context="key1"),
SecretMatch(secret_type="password_assignment", position=20, context="key2"),
]
assert should_warn_about_secrets(matches) is True
def test_no_warn_for_empty_matches(self):
"""Test no warning for empty matches."""
matches = []
assert should_warn_about_secrets(matches) is False
def test_custom_min_confidence_types(self):
"""Test custom min_confidence_types parameter."""
matches = [SecretMatch(secret_type="api_key", position=10, context="key")]
custom_types = {"api_key"}
assert should_warn_about_secrets(matches, min_confidence_types=custom_types) is True
def test_custom_min_confidence_excludes_others(self):
"""Test custom min_confidence_types excludes non-listed types."""
matches = [SecretMatch(secret_type="aws_access_key", position=10, context="key")]
custom_types = {"api_key"}
assert should_warn_about_secrets(matches, min_confidence_types=custom_types) is False
class TestRedactSecrets:
"""Tests for redact_secrets function."""
def test_redact_api_key(self):
"""Test redacting API key."""
text = "API_KEY=abcd1234efgh5678ijkl9012mnop"
redacted = redact_secrets(text)
assert "abcd1234efgh5678ijkl9012mnop" not in redacted
assert "***REDACTED***" in redacted
def test_redact_aws_access_key(self):
"""Test redacting AWS access key."""
text = "AWS key: AKIAIOSFODNN7EXAMPLE"
redacted = redact_secrets(text)
assert "AKIAIOSFODNN7EXAMPLE" not in redacted
assert "***REDACTED***" in redacted
def test_redact_openai_key(self):
"""Test redacting OpenAI key."""
text = "OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
redacted = redact_secrets(text)
assert "sk-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN" not in redacted
assert "***REDACTED***" in redacted
def test_redact_github_token(self):
"""Test redacting GitHub token."""
text = "token: ghp_1234567890abcdefghijklmnopqrstuv"
redacted = redact_secrets(text)
assert "ghp_1234567890abcdefghijklmnopqrstuv" not in redacted
assert "***REDACTED***" in redacted
def test_redact_jwt_token(self):
"""Test redacting JWT token."""
text = "JWT: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
redacted = redact_secrets(text)
assert "eyJhbGciOiJIUzI1NiJ9" not in redacted
assert "***REDACTED***" in redacted
def test_redact_database_url(self):
"""Test redacting database URL."""
text = "DB: postgres://user:pass@localhost/db"
redacted = redact_secrets(text)
assert "postgres://user:pass@localhost/db" not in redacted
assert "***REDACTED***" in redacted
def test_redact_private_key(self):
"""Test redacting private key."""
text = "Key: -----BEGIN PRIVATE KEY-----\ndata"
redacted = redact_secrets(text)
assert "-----BEGIN PRIVATE KEY-----" not in redacted
assert "***REDACTED***" in redacted
def test_redact_multiple_secrets(self):
"""Test redacting multiple secrets."""
text = "API_KEY=abc123def456ghi789jkl012 and PASSWORD=MySecret123"
redacted = redact_secrets(text)
assert "abc123def456ghi789jkl012" not in redacted
assert "MySecret123" not in redacted
assert redacted.count("***REDACTED***") >= 2
def test_redact_with_custom_replacement(self):
"""Test redacting with custom replacement string."""
text = "API_KEY=abcd1234efgh5678ijkl9012mnop"
redacted = redact_secrets(text, replacement="[HIDDEN]")
assert "abcd1234efgh5678ijkl9012mnop" not in redacted
assert "[HIDDEN]" in redacted
assert "***REDACTED***" not in redacted
def test_redact_text_without_secrets(self):
"""Test redacting text with no secrets."""
text = "This is normal text without secrets"
redacted = redact_secrets(text)
assert redacted == text
def test_redact_preserves_text_structure(self):
"""Test redacting preserves overall text structure."""
text = "Before API_KEY=abc123def456ghi789jkl012 after"
redacted = redact_secrets(text)
assert redacted.startswith("Before")
assert redacted.endswith("after")
assert "***REDACTED***" in redacted
def test_redact_empty_text(self):
"""Test redacting empty text."""
text = ""
redacted = redact_secrets(text)
assert redacted == ""
class TestCLIMain:
"""Tests for main() CLI function."""
def test_cli_scan_file_with_secrets(self, monkeypatch, capsys):
"""Test CLI scanning a file with secrets."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "config.txt"
test_file.write_text("API_KEY=abcd1234efgh5678ijkl9012mnop")
monkeypatch.setattr(sys, "argv", ["secrets", str(test_file)])
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert "WARNING" in captured.err
assert "api_key" in captured.err
def test_cli_scan_file_without_secrets(self, monkeypatch, capsys):
"""Test CLI scanning a file without secrets."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "normal.txt"
test_file.write_text("This is normal content")
monkeypatch.setattr(sys, "argv", ["secrets", str(test_file)])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "No secrets detected" in captured.err
def test_cli_scan_stdin_with_secrets(self, monkeypatch, capsys):
"""Test CLI scanning stdin with secrets."""
stdin_data = "OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets"])
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert "WARNING" in captured.err
def test_cli_scan_stdin_without_secrets(self, monkeypatch, capsys):
"""Test CLI scanning stdin without secrets."""
stdin_data = "Normal text without secrets"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets"])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "No secrets detected" in captured.err
def test_cli_redact_mode_file(self, monkeypatch, capsys):
"""Test CLI redact mode with file."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "config.txt"
test_file.write_text("API_KEY=abcd1234efgh5678ijkl9012mnop and more text")
monkeypatch.setattr(sys, "argv", ["secrets", str(test_file), "--redact"])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "***REDACTED***" in captured.out
assert "abcd1234efgh5678ijkl9012mnop" not in captured.out
def test_cli_redact_mode_stdin(self, monkeypatch, capsys):
"""Test CLI redact mode with stdin."""
stdin_data = "password=MySecret123 and other data"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets", "--redact"])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "***REDACTED***" in captured.out
assert "MySecret123" not in captured.out
def test_cli_quiet_mode_with_secrets(self, monkeypatch, capsys):
"""Test CLI quiet mode when secrets are found."""
stdin_data = "API_KEY=abcd1234efgh5678ijkl9012mnop"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets", "--quiet"])
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert captured.err == ""
def test_cli_quiet_mode_without_secrets(self, monkeypatch, capsys):
"""Test CLI quiet mode when no secrets found."""
stdin_data = "Normal text"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets", "--quiet"])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert captured.err == ""
def test_cli_max_matches_parameter(self, monkeypatch, capsys):
"""Test CLI with --max-matches parameter."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "many.txt"
content = "\n".join([f"API_KEY=abcd1234efgh5678ijkl9012mnop{i:04d}" for i in range(10)])
test_file.write_text(content)
monkeypatch.setattr(sys, "argv", ["secrets", str(test_file), "--max-matches", "3"])
exit_code = main()
assert exit_code == 1
def test_cli_file_not_found_error(self, monkeypatch, capsys):
"""Test CLI with non-existent file."""
monkeypatch.setattr(sys, "argv", ["secrets", "/nonexistent/file.txt"])
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert "Error:" in captured.err
def test_cli_combined_redact_and_quiet(self, monkeypatch, capsys):
"""Test CLI with both --redact and --quiet flags."""
stdin_data = "API_KEY=abcd1234efgh5678ijkl9012mnop"
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_data))
monkeypatch.setattr(sys, "argv", ["secrets", "--redact", "--quiet"])
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "***REDACTED***" in captured.out