mirror of
https://github.com/zenlm/claude-sanitizer.git
synced 2026-11-02 17:24:42 +00:00
316 lines
11 KiB
Python
316 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Claude Sanitizer - AI-powered intelligent sanitization
|
|
Uses Claude to intelligently detect and redact sensitive data
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import click
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
import anthropic
|
|
|
|
console = Console()
|
|
|
|
# Sensitive data patterns for pre-screening
|
|
SENSITIVE_KEYWORDS = [
|
|
'password', 'secret', 'api_key', 'token', 'private_key',
|
|
'wallet', 'seed', 'mnemonic', 'credential', 'auth',
|
|
'ssn', 'social_security', 'credit_card', 'bitcoin', 'ethereum',
|
|
'private', 'confidential', 'aws_', 'stripe_', 'oauth'
|
|
]
|
|
|
|
SANITIZATION_PROMPT = """You are an expert data sanitization system. Your task is to identify and redact sensitive information while preserving the training value of conversations.
|
|
|
|
**REDACT (replace with placeholder):**
|
|
- API keys, tokens, secrets (OpenAI, GitHub, AWS, etc.)
|
|
- Crypto seed phrases, private keys, wallet addresses
|
|
- Passwords, credentials, authentication tokens
|
|
- Credit card numbers, SSN, financial data
|
|
- Personal email addresses (non-generic)
|
|
- Phone numbers, physical addresses
|
|
- Private keys (PEM, SSH, etc.)
|
|
- Database connection strings with passwords
|
|
- OAuth tokens, JWT tokens
|
|
|
|
**PRESERVE (do NOT redact):**
|
|
- Generic emails (support@, info@, hello@)
|
|
- File paths (just redact username: /Users/[USER]/...)
|
|
- Repository names (important for context)
|
|
- Function names, class names (code patterns)
|
|
- Error messages (debugging patterns)
|
|
- Tool names (Read, Write, Bash, etc.)
|
|
- Programming concepts and terminology
|
|
- Generic variable names
|
|
|
|
**FORMAT:**
|
|
Return a JSON object with:
|
|
{
|
|
"has_sensitive_data": boolean,
|
|
"redactions": [
|
|
{
|
|
"original": "the sensitive text",
|
|
"replacement": "[APPROPRIATE_PLACEHOLDER]",
|
|
"reason": "why this needs redaction",
|
|
"severity": "high|medium|low"
|
|
}
|
|
],
|
|
"summary": "brief summary of what was found"
|
|
}
|
|
|
|
**IMPORTANT:**
|
|
- Only flag truly sensitive data that could cause security issues
|
|
- Preserve as much context as possible for training
|
|
- Use specific placeholders like [API_KEY], [SEED_PHRASE], [EMAIL], [PHONE]
|
|
- If no sensitive data, return has_sensitive_data: false with empty redactions
|
|
|
|
Here's the conversation entry to analyze:
|
|
|
|
```json
|
|
{entry}
|
|
```
|
|
|
|
Analyze this and return the JSON response."""
|
|
|
|
def get_claude_client():
|
|
"""Get Anthropic Claude client"""
|
|
api_key = os.environ.get('ANTHROPIC_API_KEY')
|
|
if not api_key:
|
|
console.print("[red]Error: ANTHROPIC_API_KEY not set[/red]")
|
|
console.print("\nSet your API key:")
|
|
console.print(" export ANTHROPIC_API_KEY=sk-ant-...")
|
|
return None
|
|
return anthropic.Anthropic(api_key=api_key)
|
|
|
|
def pre_screen(entry):
|
|
"""Quick pre-screen for potential sensitive data"""
|
|
text = json.dumps(entry).lower()
|
|
return any(keyword in text for keyword in SENSITIVE_KEYWORDS)
|
|
|
|
def sanitize_with_claude(client, entry, model="claude-3-5-haiku-20241022"):
|
|
"""Use Claude to intelligently sanitize an entry"""
|
|
try:
|
|
response = client.messages.create(
|
|
model=model,
|
|
max_tokens=2000,
|
|
messages=[{
|
|
"role": "user",
|
|
"content": SANITIZATION_PROMPT.format(entry=json.dumps(entry, indent=2))
|
|
}]
|
|
)
|
|
|
|
# Parse Claude's response
|
|
result_text = response.content[0].text
|
|
|
|
# Extract JSON from response (might be wrapped in markdown)
|
|
if '```json' in result_text:
|
|
result_text = result_text.split('```json')[1].split('```')[0].strip()
|
|
elif '```' in result_text:
|
|
result_text = result_text.split('```')[1].split('```')[0].strip()
|
|
|
|
result = json.loads(result_text)
|
|
return result
|
|
|
|
except Exception as e:
|
|
console.print(f"[yellow]Warning: Claude analysis failed: {e}[/yellow]")
|
|
return {
|
|
"has_sensitive_data": False,
|
|
"redactions": [],
|
|
"summary": f"Analysis failed: {str(e)}"
|
|
}
|
|
|
|
def apply_redactions(entry, redactions):
|
|
"""Apply redactions to entry"""
|
|
entry_str = json.dumps(entry)
|
|
|
|
for redaction in redactions:
|
|
entry_str = entry_str.replace(redaction['original'], redaction['replacement'])
|
|
|
|
return json.loads(entry_str)
|
|
|
|
@click.command()
|
|
@click.argument('input_file', type=click.Path(exists=True))
|
|
@click.option('--output', '-o', type=click.Path(), help='Output file (default: INPUT_sanitized.jsonl)')
|
|
@click.option('--model', '-m', default='claude-3-5-haiku-20241022',
|
|
help='Claude model (haiku/sonnet)')
|
|
@click.option('--aggressive', is_flag=True, help='Check every entry (slower, more thorough)')
|
|
@click.option('--report', '-r', type=click.Path(), help='Save detailed report')
|
|
@click.option('--dry-run', is_flag=True, help='Show what would be redacted without saving')
|
|
def main(input_file, output, model, aggressive, report, dry_run):
|
|
"""
|
|
🔐 Claude Sanitizer - AI-powered intelligent sanitization
|
|
|
|
Uses Claude to intelligently detect and redact sensitive data while
|
|
preserving the training value of conversations.
|
|
|
|
\b
|
|
Models:
|
|
haiku (default) - Fast, cost-effective (claude-3-5-haiku-20241022)
|
|
sonnet - More thorough (claude-3-5-sonnet-20241022)
|
|
|
|
\b
|
|
Usage:
|
|
claude-sanitizer my-data.jsonl
|
|
claude-sanitizer my-data.jsonl --model sonnet --aggressive
|
|
claude-sanitizer my-data.jsonl --dry-run --report report.json
|
|
"""
|
|
|
|
console.print("\n[bold cyan]🔐 Claude Sanitizer v0.1.0[/bold cyan]")
|
|
console.print("[dim]AI-powered intelligent data sanitization[/dim]\n")
|
|
|
|
# Get Claude client
|
|
client = get_claude_client()
|
|
if not client:
|
|
return
|
|
|
|
# Map model names
|
|
model_map = {
|
|
'haiku': 'claude-3-5-haiku-20241022',
|
|
'sonnet': 'claude-3-5-sonnet-20241022'
|
|
}
|
|
model = model_map.get(model, model)
|
|
|
|
console.print(f"[cyan]Model:[/cyan] {model}")
|
|
console.print(f"[cyan]Mode:[/cyan] {'Aggressive (all entries)' if aggressive else 'Smart (pre-screened)'}\n")
|
|
|
|
# Read input
|
|
input_path = Path(input_file)
|
|
entries = []
|
|
|
|
with open(input_path, 'r') as f:
|
|
for line in f:
|
|
if line.strip():
|
|
entries.append(json.loads(line))
|
|
|
|
console.print(f"[green]✓[/green] Loaded {len(entries)} entries\n")
|
|
|
|
# Process entries
|
|
sanitized_entries = []
|
|
redaction_report = []
|
|
stats = {
|
|
'total': len(entries),
|
|
'checked': 0,
|
|
'has_sensitive': 0,
|
|
'redactions': 0,
|
|
'cost_estimate': 0.0
|
|
}
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
console=console
|
|
) as progress:
|
|
task = progress.add_task("Sanitizing...", total=len(entries))
|
|
|
|
for idx, entry in enumerate(entries):
|
|
# Pre-screen unless aggressive mode
|
|
should_check = aggressive or pre_screen(entry)
|
|
|
|
if should_check:
|
|
stats['checked'] += 1
|
|
|
|
# Analyze with Claude
|
|
result = sanitize_with_claude(client, entry, model)
|
|
|
|
if result['has_sensitive_data'] and result['redactions']:
|
|
stats['has_sensitive'] += 1
|
|
stats['redactions'] += len(result['redactions'])
|
|
|
|
# Apply redactions
|
|
sanitized_entry = apply_redactions(entry, result['redactions'])
|
|
sanitized_entries.append(sanitized_entry)
|
|
|
|
# Add to report
|
|
redaction_report.append({
|
|
'entry_index': idx,
|
|
'analysis': result
|
|
})
|
|
|
|
else:
|
|
sanitized_entries.append(entry)
|
|
else:
|
|
# No sensitive keywords found, keep as-is
|
|
sanitized_entries.append(entry)
|
|
|
|
progress.update(task, advance=1)
|
|
|
|
# Cost estimate (rough)
|
|
# Haiku: $0.25/1M input, $1.25/1M output
|
|
# Sonnet: $3/1M input, $15/1M output
|
|
avg_tokens_per_check = 500 # rough estimate
|
|
if 'haiku' in model:
|
|
stats['cost_estimate'] = (stats['checked'] * avg_tokens_per_check / 1_000_000) * 0.25
|
|
else:
|
|
stats['cost_estimate'] = (stats['checked'] * avg_tokens_per_check / 1_000_000) * 3
|
|
|
|
# Display results
|
|
console.print()
|
|
table = Table(title="Sanitization Results")
|
|
table.add_column("Metric", style="cyan")
|
|
table.add_column("Value", style="green")
|
|
|
|
table.add_row("Total entries", f"{stats['total']:,}")
|
|
table.add_row("Entries checked", f"{stats['checked']:,}")
|
|
table.add_row("With sensitive data", f"{stats['has_sensitive']:,}")
|
|
table.add_row("Total redactions", f"{stats['redactions']:,}")
|
|
table.add_row("Estimated cost", f"${stats['cost_estimate']:.4f}")
|
|
|
|
console.print(table)
|
|
console.print()
|
|
|
|
# Show sample redactions
|
|
if redaction_report:
|
|
console.print("[bold]Sample Redactions:[/bold]\n")
|
|
for item in redaction_report[:3]:
|
|
for red in item['analysis']['redactions'][:2]:
|
|
severity_color = {
|
|
'high': 'red',
|
|
'medium': 'yellow',
|
|
'low': 'blue'
|
|
}.get(red['severity'], 'white')
|
|
|
|
console.print(f" [{severity_color}]●[/{severity_color}] {red['replacement']}: {red['reason']}")
|
|
console.print()
|
|
|
|
# Save results
|
|
if not dry_run:
|
|
# Output file
|
|
if not output:
|
|
output = input_path.parent / f"{input_path.stem}_sanitized{input_path.suffix}"
|
|
|
|
with open(output, 'w') as f:
|
|
for entry in sanitized_entries:
|
|
f.write(json.dumps(entry) + '\n')
|
|
|
|
console.print(f"[green]✅ Sanitized data saved:[/green] {output}")
|
|
|
|
# Report file
|
|
if report or redaction_report:
|
|
report_path = report or input_path.parent / f"{input_path.stem}_report.json"
|
|
with open(report_path, 'w') as f:
|
|
json.dump({
|
|
'stats': stats,
|
|
'redactions': redaction_report,
|
|
'timestamp': datetime.now().isoformat()
|
|
}, f, indent=2)
|
|
console.print(f"[green]✅ Report saved:[/green] {report_path}")
|
|
|
|
else:
|
|
console.print("[yellow]🔍 DRY RUN - No files saved[/yellow]")
|
|
|
|
console.print()
|
|
console.print(Panel.fit(
|
|
"[bold green]✨ Sanitization complete![/bold green]\n\n"
|
|
"Your data is now safe for contribution while preserving\n"
|
|
"maximum training value for agentic AI development.",
|
|
title="Success"
|
|
))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|