mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-16 05:37:50 +00:00
Make things 'betterer' across the board
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// Package privacy provides utilities for protecting sensitive data.
|
||||
package privacy
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// secretPatterns contains compiled regular expressions for detecting secrets.
|
||||
// These patterns are designed to catch common secret formats with minimal false positives.
|
||||
var secretPatterns = []*regexp.Regexp{
|
||||
// API keys with common prefixes
|
||||
regexp.MustCompile(`(?i)(api[_-]?key|apikey)\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?`),
|
||||
|
||||
// Passwords in configuration
|
||||
regexp.MustCompile(`(?i)(password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]`),
|
||||
|
||||
// Secret tokens
|
||||
regexp.MustCompile(`(?i)(secret[_-]?key|secret[_-]?token|auth[_-]?token)\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?`),
|
||||
|
||||
// OpenAI API keys
|
||||
regexp.MustCompile(`sk-[a-zA-Z0-9]{20,}`),
|
||||
|
||||
// Anthropic API keys
|
||||
regexp.MustCompile(`sk-ant-[a-zA-Z0-9-]{20,}`),
|
||||
|
||||
// GitHub tokens
|
||||
regexp.MustCompile(`gh[pous]_[a-zA-Z0-9]{36,}`),
|
||||
regexp.MustCompile(`github_pat_[a-zA-Z0-9_]{22,}`),
|
||||
|
||||
// AWS keys
|
||||
regexp.MustCompile(`AKIA[0-9A-Z]{16}`),
|
||||
regexp.MustCompile(`(?i)aws[_-]?secret[_-]?access[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9/+=]{40}['"]?`),
|
||||
|
||||
// Private keys (PEM format indicators)
|
||||
regexp.MustCompile(`-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----`),
|
||||
|
||||
// JWT tokens (base64.base64.base64 format)
|
||||
regexp.MustCompile(`eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+`),
|
||||
|
||||
// Generic secret assignment patterns
|
||||
regexp.MustCompile(`(?i)bearer\s+[a-zA-Z0-9_-]{20,}`),
|
||||
}
|
||||
|
||||
// ContainsSecrets checks if the given text contains any patterns that look like secrets.
|
||||
// Returns true if potential secrets are detected.
|
||||
func ContainsSecrets(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, pattern := range secretPatterns {
|
||||
if pattern.MatchString(text) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RedactSecrets replaces detected secrets with a redaction marker.
|
||||
// This allows the text to be stored while protecting sensitive data.
|
||||
func RedactSecrets(text string) string {
|
||||
if text == "" {
|
||||
return text
|
||||
}
|
||||
|
||||
result := text
|
||||
for _, pattern := range secretPatterns {
|
||||
result = pattern.ReplaceAllStringFunc(result, func(match string) string {
|
||||
// Preserve the key name, redact only the value
|
||||
if idx := strings.Index(match, "="); idx != -1 {
|
||||
return match[:idx+1] + "[REDACTED]"
|
||||
}
|
||||
if idx := strings.Index(match, ":"); idx != -1 {
|
||||
return match[:idx+1] + "[REDACTED]"
|
||||
}
|
||||
// For standalone secrets, show just the prefix
|
||||
if len(match) > 8 {
|
||||
return match[:4] + "...[REDACTED]"
|
||||
}
|
||||
return "[REDACTED]"
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SanitizeObservation checks multiple fields of an observation for secrets.
|
||||
// Returns true if any secrets were found.
|
||||
// This function is used as a validation gate before storing observations.
|
||||
func SanitizeObservation(narrative string, facts []string) bool {
|
||||
if ContainsSecrets(narrative) {
|
||||
return true
|
||||
}
|
||||
return slices.ContainsFunc(facts, func(fact string) bool {
|
||||
return ContainsSecrets(fact)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package privacy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainsSecrets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "normal text",
|
||||
input: "This is just some regular text about a bug fix",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "API key pattern",
|
||||
input: "api_key=abc123def456ghi789jkl012mno345pqr678",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "api-key with dash",
|
||||
input: `api-key: "abc123def456ghi789jkl012mno"`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "password in config",
|
||||
input: `password="super_secret_password_123"`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "OpenAI key format",
|
||||
input: "sk-abc123def456ghi789jkl012mno345pqr678",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Anthropic key format",
|
||||
input: "sk-ant-api03-abc123def456ghi789jkl012mno345",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub PAT",
|
||||
input: "ghp_1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub PAT new format",
|
||||
input: "github_pat_12ABCDEFGHIJ3456789abc_defghijklmno",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "AWS access key",
|
||||
input: "AKIAIOSFODNN7EXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Private key header",
|
||||
input: "-----BEGIN RSA PRIVATE KEY-----",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "JWT token",
|
||||
input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "bearer token",
|
||||
input: "Bearer abc123def456ghi789jkl012mno345",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "secret_key in code",
|
||||
input: `secret_key = "my_super_secret_token_here"`,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "short password is not detected",
|
||||
input: `password="short"`,
|
||||
expected: false, // Too short to trigger
|
||||
},
|
||||
{
|
||||
name: "word password in sentence",
|
||||
input: "The password field should be validated",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "word api in code",
|
||||
input: "The API returns JSON data",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ContainsSecrets(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ContainsSecrets(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactSecrets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "no secrets",
|
||||
input: "This is safe text",
|
||||
expected: "This is safe text",
|
||||
},
|
||||
{
|
||||
name: "API key gets redacted",
|
||||
input: "api_key=abc123def456ghi789jkl012mno345pqr678",
|
||||
expected: "api_key=[REDACTED]",
|
||||
},
|
||||
{
|
||||
name: "OpenAI key gets redacted",
|
||||
input: "The key is sk-abc123def456ghi789jkl012mno345pqr678",
|
||||
expected: "The key is sk-a...[REDACTED]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := RedactSecrets(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactSecrets(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeObservation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
narrative string
|
||||
facts []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "clean observation",
|
||||
narrative: "Fixed a bug in the login flow",
|
||||
facts: []string{"Users can now log in", "Session management improved"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "secret in narrative",
|
||||
narrative: "Set API key api_key=abc123def456ghi789jkl012mno345",
|
||||
facts: []string{"Configuration updated"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "secret in facts",
|
||||
narrative: "Updated configuration",
|
||||
facts: []string{"Added api_key=abc123def456ghi789jkl012mno345"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty facts",
|
||||
narrative: "Clean narrative",
|
||||
facts: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil facts",
|
||||
narrative: "Clean narrative",
|
||||
facts: nil,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeObservation(tt.narrative, tt.facts)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeObservation() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContainsSecrets(b *testing.B) {
|
||||
text := "This is a normal piece of text that does not contain any secrets or sensitive information"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ContainsSecrets(text)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContainsSecretsWithSecret(b *testing.B) {
|
||||
text := "api_key=abc123def456ghi789jkl012mno345pqr678"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ContainsSecrets(text)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user