mirror of
https://github.com/zenlm/claude-sanitizer.git
synced 2026-10-30 15:00:25 +00:00
322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""
|
|
Tests for claude-sanitizer sanitization functionality.
|
|
|
|
These tests verify that sensitive data is properly detected and redacted.
|
|
"""
|
|
|
|
import json
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
from claude_sanitizer.cli import sanitize_with_claude, SENSITIVE_KEYWORDS
|
|
|
|
|
|
class TestSensitiveKeywordDetection:
|
|
"""Test that sensitive keywords are properly detected."""
|
|
|
|
def test_detects_api_key_mentions(self):
|
|
"""Should flag entries mentioning API keys."""
|
|
entry = {"content": "Here's my api_key for the service"}
|
|
assert any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS)
|
|
|
|
def test_detects_password_mentions(self):
|
|
"""Should flag entries mentioning passwords."""
|
|
entry = {"content": "The password is stored here"}
|
|
assert any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS)
|
|
|
|
def test_detects_secret_mentions(self):
|
|
"""Should flag entries mentioning secrets."""
|
|
entry = {"content": "secret configuration value"}
|
|
assert any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS)
|
|
|
|
def test_detects_token_mentions(self):
|
|
"""Should flag entries mentioning tokens."""
|
|
entry = {"content": "authentication token required"}
|
|
assert any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS)
|
|
|
|
def test_detects_private_key_mentions(self):
|
|
"""Should flag entries mentioning private keys."""
|
|
entry = {"content": "-----BEGIN PRIVATE KEY-----"}
|
|
assert any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS)
|
|
|
|
def test_ignores_safe_content(self):
|
|
"""Should not flag safe programming content."""
|
|
entry = {"content": "function calculateTotal(items) { return sum; }"}
|
|
# This might have 'total' but context is safe
|
|
# The AI layer will determine it's safe
|
|
assert not any(kw in entry["content"].lower() for kw in SENSITIVE_KEYWORDS if kw not in ["function", "return"])
|
|
|
|
|
|
class TestClaudeSanitization:
|
|
"""Test AI-powered sanitization with Claude."""
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_redacts_api_keys(self, mock_anthropic):
|
|
"""Should detect and redact API keys."""
|
|
# Mock Claude response
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "api_key",
|
|
"location": "OpenAI API key in message",
|
|
"placeholder": "[OPENAI_API_KEY]",
|
|
"severity": "high",
|
|
"reason": "Actual API key detected"
|
|
}],
|
|
"summary": "Found 1 API key"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "My key is sk-1234567890abcdef"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is True
|
|
assert len(result["redactions"]) == 1
|
|
assert result["redactions"][0]["type"] == "api_key"
|
|
assert result["redactions"][0]["severity"] == "high"
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_redacts_passwords(self, mock_anthropic):
|
|
"""Should detect and redact actual passwords."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "password",
|
|
"location": "Database connection string",
|
|
"placeholder": "[PASSWORD]",
|
|
"severity": "high",
|
|
"reason": "Hardcoded password in connection string"
|
|
}],
|
|
"summary": "Found 1 password"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "DB_URL=postgresql://user:MyP@ssw0rd@localhost/db"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is True
|
|
assert result["redactions"][0]["type"] == "password"
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_preserves_code_examples(self, mock_anthropic):
|
|
"""Should NOT redact 'password' in code examples."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": False,
|
|
"redactions": [],
|
|
"summary": "No sensitive data - 'password' is a variable name in code"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": 'const password = prompt("Enter password:");'}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is False
|
|
assert len(result["redactions"]) == 0
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_redacts_crypto_seeds(self, mock_anthropic):
|
|
"""Should detect and redact crypto seed phrases."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "crypto_seed",
|
|
"location": "12-word seed phrase",
|
|
"placeholder": "[SEED_PHRASE]",
|
|
"severity": "high",
|
|
"reason": "BIP39 seed phrase detected"
|
|
}],
|
|
"summary": "Found 1 seed phrase"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "My seed: abandon ability able about above absent"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is True
|
|
assert result["redactions"][0]["type"] == "crypto_seed"
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_redacts_private_keys(self, mock_anthropic):
|
|
"""Should detect and redact private keys."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "private_key",
|
|
"location": "64-character hex private key",
|
|
"placeholder": "[PRIVATE_KEY]",
|
|
"severity": "high",
|
|
"reason": "Ethereum private key format"
|
|
}],
|
|
"summary": "Found 1 private key"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "0x" + "a" * 64}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is True
|
|
assert result["redactions"][0]["type"] == "private_key"
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_preserves_file_paths(self, mock_anthropic):
|
|
"""Should preserve file paths as training context."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": False,
|
|
"redactions": [],
|
|
"summary": "File paths preserved - important for context"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "Reading from /Users/dev/project/src/auth.py"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is False
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_preserves_function_names(self, mock_anthropic):
|
|
"""Should preserve function names for training."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": False,
|
|
"redactions": [],
|
|
"summary": "Function names preserved for training"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "def authenticate_user(username, password_hash):"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is False
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_handles_edge_cases(self, mock_anthropic):
|
|
"""Should handle edge cases gracefully."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": False,
|
|
"redactions": [],
|
|
"summary": "No issues found"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
# Empty content
|
|
result = sanitize_with_claude(mock_client, {"content": ""})
|
|
assert result["has_sensitive_data"] is False
|
|
|
|
# Very long content
|
|
long_content = "x" * 10000
|
|
result = sanitize_with_claude(mock_client, {"content": long_content})
|
|
assert result is not None
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for the complete workflow."""
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_end_to_end_sanitization(self, mock_anthropic):
|
|
"""Test complete sanitization workflow."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [
|
|
{
|
|
"type": "api_key",
|
|
"location": "Line 5",
|
|
"placeholder": "[OPENAI_API_KEY]",
|
|
"severity": "high",
|
|
"reason": "OpenAI API key"
|
|
},
|
|
{
|
|
"type": "email",
|
|
"location": "Line 10",
|
|
"placeholder": "[EMAIL]",
|
|
"severity": "medium",
|
|
"reason": "Personal email address"
|
|
}
|
|
],
|
|
"summary": "Found 2 sensitive items"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {
|
|
"messages": [
|
|
{"role": "user", "content": "My API key is sk-test123"},
|
|
{"role": "assistant", "content": "Contact me at [email protected]"}
|
|
]
|
|
}
|
|
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["has_sensitive_data"] is True
|
|
assert len(result["redactions"]) == 2
|
|
assert result["redactions"][0]["severity"] == "high"
|
|
assert result["redactions"][1]["severity"] == "medium"
|
|
|
|
|
|
class TestSeverityLevels:
|
|
"""Test that severity levels are properly assigned."""
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_high_severity_for_crypto(self, mock_anthropic):
|
|
"""Crypto data should be high severity."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "crypto_seed",
|
|
"location": "Seed phrase",
|
|
"placeholder": "[SEED_PHRASE]",
|
|
"severity": "high",
|
|
"reason": "BIP39 seed phrase"
|
|
}],
|
|
"summary": "Critical crypto data"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "wallet seed phrase here"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["redactions"][0]["severity"] == "high"
|
|
|
|
@patch('anthropic.Anthropic')
|
|
def test_medium_severity_for_pii(self, mock_anthropic):
|
|
"""PII should be medium severity."""
|
|
mock_client = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = [Mock(text=json.dumps({
|
|
"has_sensitive_data": True,
|
|
"redactions": [{
|
|
"type": "email",
|
|
"location": "Contact info",
|
|
"placeholder": "[EMAIL]",
|
|
"severity": "medium",
|
|
"reason": "Personal email"
|
|
}],
|
|
"summary": "PII detected"
|
|
}))]
|
|
mock_client.messages.create.return_value = mock_response
|
|
|
|
entry = {"content": "Email: [email protected]"}
|
|
result = sanitize_with_claude(mock_client, entry)
|
|
|
|
assert result["redactions"][0]["severity"] == "medium"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|