Skip to content

Commit c3c1cac

Browse files
security: Expand protection patterns for comprehensive scanning
Now scans ALL staged files for sensitive content, not just protected files. Categories now covered: - API keys (Anthropic, OpenAI, Perplexity, ElevenLabs, Google, Cloudflare, GitHub) - Bearer tokens and secret key formats - Private key files (RSA, OpenSSH, PGP, EC) - Personal paths (/Users/daniel/.claude/*) - Named contacts (full names only, not first names) - SSN format - Slack tokens (xoxb/xoxp) Added exception_files for documentation (README.md, INSTALL.md) to prevent false positives on conceptual mentions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5adc3ed commit c3c1cac

2 files changed

Lines changed: 133 additions & 9 deletions

File tree

.pai-protected.json

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,52 @@
4141
"validation": "These directories contain private Kai data and must not be committed"
4242
},
4343
"protected_patterns": {
44-
"description": "String patterns that should never appear in PAI",
44+
"description": "String patterns that should never appear in PAI - actual data, not documentation mentions",
4545
"patterns": [
46-
"daniel@danielmiessler.com",
47-
"susan@danielmiessler.com",
46+
"daniel@danielmiessler\\.com",
47+
"susan@danielmiessler\\.com",
48+
"[a-z]+@danielmiessler\\.com",
49+
"[a-z]+@unsupervised-learning\\.com",
4850
"ANTHROPIC_API_KEY=sk-",
49-
"PERPLEXITY_API_KEY=[^E]",
50-
"ELEVENLABS_API_KEY=[^y]",
51-
"GOOGLE_API_KEY=[^y]",
51+
"OPENAI_API_KEY=sk-",
52+
"PERPLEXITY_API_KEY=pplx-",
53+
"ELEVENLABS_API_KEY=[a-f0-9]",
54+
"GOOGLE_API_KEY=AIza",
55+
"CLOUDFLARE_API_TOKEN=[a-zA-Z0-9_-]{40}",
56+
"GITHUB_TOKEN=gh[ps]_",
57+
"API_KEY=['\"][a-zA-Z0-9_-]{16,}['\"]",
58+
"SECRET_KEY=['\"][a-zA-Z0-9_-]{16,}['\"]",
59+
"Bearer [a-zA-Z0-9_-]{20,}",
60+
"sk-[a-zA-Z0-9]{20,}",
61+
"ghp_[a-zA-Z0-9]{36}",
62+
"gho_[a-zA-Z0-9]{36}",
63+
"xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+",
64+
"xoxp-[0-9]+-[0-9]+-[a-zA-Z0-9]+",
5265
"/Users/daniel/.claude/skills/personal",
5366
"/Users/daniel/.claude/MEMORY",
5467
"/Users/daniel/.claude/History",
55-
"daemon.plist",
56-
"~/.claude/skills/CORE/USER"
68+
"/Users/daniel/.claude/skills/CORE/USER",
69+
"~/.claude/skills/CORE/USER/",
70+
"daemon\\.plist",
71+
"Kaleigh Feher",
72+
"Bryan Brake",
73+
"Angela Gunn",
74+
"Susan Miessler",
75+
"SSN[:\\s]+[0-9]{3}-[0-9]{2}-[0-9]{4}",
76+
"-----BEGIN RSA PRIVATE KEY-----",
77+
"-----BEGIN OPENSSH PRIVATE KEY-----",
78+
"-----BEGIN PGP PRIVATE KEY-----",
79+
"-----BEGIN EC PRIVATE KEY-----",
80+
"id_rsa\\.pub",
81+
"id_ed25519\\.pub"
5782
],
5883
"exception_files": [
5984
"SECURITY.md",
6085
".pai-protected.json",
6186
".env.example",
62-
"Tools/validate-protected.ts"
87+
"Tools/validate-protected.ts",
88+
"README.md",
89+
"INSTALL.md"
6390
]
6491
}
6592
},

Tools/validate-protected.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,74 @@ function checkForbiddenDirectories(stagedFiles: string[], manifest: ProtectedMan
7878
return violations;
7979
}
8080

81+
function scanAllFilesForSensitiveContent(stagedFiles: string[], manifest: ProtectedManifest): {
82+
file: string;
83+
violations: string[];
84+
}[] {
85+
const results: { file: string; violations: string[] }[] = [];
86+
const patternCategory = manifest.protected.protected_patterns;
87+
88+
if (!patternCategory || !patternCategory.patterns) {
89+
return results;
90+
}
91+
92+
const exceptions = patternCategory.exception_files || [];
93+
94+
for (const file of stagedFiles) {
95+
// Skip exception files
96+
if (exceptions.includes(file)) {
97+
continue;
98+
}
99+
100+
const fullPath = join(PAI_ROOT, file);
101+
102+
// Skip if file doesn't exist (might be deleted)
103+
if (!existsSync(fullPath)) {
104+
continue;
105+
}
106+
107+
// Skip binary files
108+
try {
109+
const content = readFileSync(fullPath, 'utf-8');
110+
const violations: string[] = [];
111+
112+
for (const pattern of patternCategory.patterns) {
113+
try {
114+
const regex = new RegExp(pattern, 'gi');
115+
const matches = content.match(regex);
116+
117+
if (matches) {
118+
// Get line numbers for context
119+
const lines = content.split('\n');
120+
const matchingLines: number[] = [];
121+
lines.forEach((line, idx) => {
122+
if (regex.test(line)) {
123+
matchingLines.push(idx + 1);
124+
}
125+
regex.lastIndex = 0; // Reset regex state
126+
});
127+
128+
const lineInfo = matchingLines.length > 0
129+
? ` (lines: ${matchingLines.slice(0, 3).join(', ')}${matchingLines.length > 3 ? '...' : ''})`
130+
: '';
131+
violations.push(`Sensitive pattern "${pattern}"${lineInfo}`);
132+
}
133+
} catch {
134+
// Invalid regex, skip
135+
}
136+
}
137+
138+
if (violations.length > 0) {
139+
results.push({ file, violations });
140+
}
141+
} catch {
142+
// Binary file or read error, skip
143+
}
144+
}
145+
146+
return results;
147+
}
148+
81149
function getAllProtectedFiles(manifest: ProtectedManifest): string[] {
82150
const files: string[] = [];
83151

@@ -202,6 +270,35 @@ async function main() {
202270
process.exit(1);
203271
}
204272

273+
// Scan ALL staged files for sensitive content
274+
console.log(`\n${YELLOW}Scanning ${stagedFiles.length} staged file(s) for sensitive content...${RESET}\n`);
275+
const sensitiveResults = scanAllFilesForSensitiveContent(stagedFiles, manifest);
276+
277+
if (sensitiveResults.length > 0) {
278+
console.log(`${RED}🚫 SENSITIVE CONTENT DETECTED${RESET}\n`);
279+
for (const result of sensitiveResults) {
280+
console.log(`${RED}${RESET} ${result.file}`);
281+
for (const violation of result.violations) {
282+
console.log(` ${RED}${RESET} ${violation}`);
283+
}
284+
}
285+
console.log('\n' + '='.repeat(60));
286+
console.log(`\n${RED}🚫 COMMIT BLOCKED${RESET}\n`);
287+
console.log('Files contain sensitive content that must NOT be in public PAI:');
288+
console.log(' - API keys, tokens, secrets');
289+
console.log(' - Personal information (emails, contacts, family)');
290+
console.log(' - Customer/confidential data');
291+
console.log(' - Private file paths');
292+
console.log('\n' + YELLOW + 'To fix:' + RESET);
293+
console.log(' 1. Remove or redact the sensitive content');
294+
console.log(' 2. Use placeholder values (e.g., "your_api_key_here")');
295+
console.log(' 3. Add file to exception_files if it\'s intentional\n');
296+
process.exit(1);
297+
}
298+
299+
console.log(`${GREEN}✅ No sensitive content found in staged files${RESET}\n`);
300+
console.log('='.repeat(60));
301+
205302
filesToCheck = allProtectedFiles.filter(f => stagedFiles.includes(f));
206303

207304
if (filesToCheck.length === 0) {

0 commit comments

Comments
 (0)