Initial commit: Opencode Filter

This commit is contained in:
2026-04-10 21:57:22 -07:00
commit 9ca7f83c91
63 changed files with 21272 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
/**
* Realistic Secret Corpus Test
*
* Validates detection accuracy on 100+ realistic secret examples.
* Tests against real-world patterns from GitHub Secret Scanning,
* TruffleHog, and GitLeaks test data.
*
* Target: >85% detection accuracy
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { readFileSync, readdirSync } from 'fs';
import { join, basename } from 'path';
import { fileURLToPath } from 'url';
import { RegexEngine } from '../src/patterns/regex-engine';
import { EntropyEngine } from '../src/entropy';
import { SecretDetector } from '../src/detector';
import type { DetectedSecret } from '../src/types';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES_DIR = join(__dirname, 'fixtures/realistic-secrets');
interface SecretExample {
value: string;
category: string;
line: number;
source: string;
}
interface CategoryResult {
total: number;
detected: number;
accuracy: number;
secrets: SecretExample[];
}
interface CorpusResults {
total: number;
detected: number;
accuracy: number;
byCategory: Record<string, CategoryResult>;
}
/**
* Parse fixture files and extract secret examples
* Ignores comment lines (starting with #) and empty lines
*/
function loadSecretCorpus(): SecretExample[] {
const files = readdirSync(FIXTURES_DIR).filter(f => f.endsWith('.txt'));
const examples: SecretExample[] = [];
for (const file of files) {
const category = basename(file, '.txt');
const content = readFileSync(join(FIXTURES_DIR, file), 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip comments, empty lines, and continuation lines
if (!line || line.startsWith('#')) continue;
// Skip lines that are clearly continuation of multiline secrets
if (line.startsWith('-----') || line.includes('= ') && !line.includes(':') && !line.includes('=')) {
continue;
}
examples.push({
value: line,
category,
line: i + 1,
source: file,
});
}
}
return examples;
}
/**
* Check if a secret value is detected by the engine
*/
function isDetected(secret: SecretExample, detector: SecretDetector): boolean {
const text = secret.value;
const results = detector.detect(text);
// A secret is detected if any result overlaps with the secret value
return results.length > 0;
}
/**
* Get detailed detection results for a secret
*/
function getDetectionDetails(secret: SecretExample, detector: SecretDetector): DetectedSecret[] {
const text = secret.value;
return detector.detect(text);
}
describe('Realistic Secret Corpus', () => {
let detector: SecretDetector;
let corpus: SecretExample[];
let results: CorpusResults;
beforeAll(() => {
// Initialize detector with regex and entropy engines
const regexEngine = new RegexEngine();
const entropyEngine = new EntropyEngine(4.5, 16);
detector = new SecretDetector(regexEngine, entropyEngine);
// Load corpus
corpus = loadSecretCorpus();
// Initialize results
results = {
total: corpus.length,
detected: 0,
accuracy: 0,
byCategory: {},
};
// Run detection on all examples
for (const secret of corpus) {
if (!results.byCategory[secret.category]) {
results.byCategory[secret.category] = {
total: 0,
detected: 0,
accuracy: 0,
secrets: [],
};
}
results.byCategory[secret.category].total++;
results.byCategory[secret.category].secrets.push(secret);
if (isDetected(secret, detector)) {
results.detected++;
results.byCategory[secret.category].detected++;
}
}
// Calculate accuracy percentages
results.accuracy = (results.detected / results.total) * 100;
for (const category of Object.keys(results.byCategory)) {
const cat = results.byCategory[category];
cat.accuracy = (cat.detected / cat.total) * 100;
}
});
describe('Corpus Statistics', () => {
it('should have loaded at least 100 secret examples', () => {
expect(results.total).toBeGreaterThanOrEqual(100);
});
it('should detect secrets in all categories', () => {
const categories = Object.keys(results.byCategory);
expect(categories.length).toBeGreaterThanOrEqual(8);
});
it('should print corpus statistics', () => {
console.log('\n=== Realistic Secret Corpus Results ===\n');
console.log(`Total Examples: ${results.total}`);
console.log(`Total Detected: ${results.detected}`);
console.log(`Overall Accuracy: ${results.accuracy.toFixed(2)}%\n`);
console.log('By Category:');
console.table(
Object.entries(results.byCategory).map(([name, data]) => ({
Category: name,
Total: data.total,
Detected: data.detected,
Accuracy: `${data.accuracy.toFixed(1)}%`,
}))
);
// Print undetected examples for debugging
const undetected: Record<string, SecretExample[]> = {};
for (const [category, data] of Object.entries(results.byCategory)) {
const missed = data.secrets.filter(s => !isDetected(s, detector));
if (missed.length > 0) {
undetected[category] = missed.slice(0, 3); // Show first 3 per category
}
}
if (Object.keys(undetected).length > 0) {
console.log('\n=== Undetected Examples (first 3 per category) ===\n');
for (const [category, examples] of Object.entries(undetected)) {
console.log(`\n${category}:`);
examples.forEach(ex => {
console.log(` Line ${ex.line}: ${ex.value.substring(0, 50)}${ex.value.length > 50 ? '...' : ''}`);
});
}
}
});
});
describe('Overall Accuracy', () => {
it('should achieve >85% detection accuracy', () => {
expect(results.accuracy).toBeGreaterThan(85);
});
});
describe('Category Accuracy', () => {
const criticalCategories = ['aws-keys', 'github-tokens', 'stripe-keys'];
for (const category of criticalCategories) {
it(`should detect ${category} with >80% accuracy`, () => {
const cat = results.byCategory[category];
expect(cat).toBeDefined();
expect(cat.accuracy).toBeGreaterThan(80);
});
}
it('should detect jwt-tokens with reasonable accuracy', () => {
const cat = results.byCategory['jwt-tokens'];
expect(cat).toBeDefined();
expect(cat.accuracy).toBeGreaterThan(70);
});
it('should detect slack-tokens with reasonable accuracy', () => {
const cat = results.byCategory['slack-tokens'];
expect(cat).toBeDefined();
expect(cat.accuracy).toBeGreaterThan(70);
});
it('should detect database-urls with reasonable accuracy', () => {
const cat = results.byCategory['database-urls'];
expect(cat).toBeDefined();
expect(cat.accuracy).toBeGreaterThan(70);
});
});
describe('Specific Detection Tests', () => {
it('should detect AWS Access Key IDs', () => {
const awsExamples = corpus.filter(s =>
s.category === 'aws-keys' && s.value.startsWith('AKIA')
);
expect(awsExamples.length).toBeGreaterThan(0);
const detected = awsExamples.filter(s => isDetected(s, detector));
expect(detected.length / awsExamples.length).toBeGreaterThan(0.8);
});
it('should detect GitHub Personal Access Tokens', () => {
const githubExamples = corpus.filter(s =>
s.category === 'github-tokens' && s.value.startsWith('ghp_')
);
expect(githubExamples.length).toBeGreaterThan(0);
const detected = githubExamples.filter(s => isDetected(s, detector));
expect(detected.length / githubExamples.length).toBeGreaterThan(0.8);
});
it('should detect Stripe Live Keys', () => {
const stripeExamples = corpus.filter(s =>
s.category === 'stripe-keys' && s.value.startsWith('sk_live_')
);
expect(stripeExamples.length).toBeGreaterThan(0);
const detected = stripeExamples.filter(s => isDetected(s, detector));
expect(detected.length / stripeExamples.length).toBeGreaterThan(0.8);
});
it('should detect JWT tokens', () => {
const jwtExamples = corpus.filter(s =>
s.category === 'jwt-tokens' && s.value.startsWith('eyJ')
);
expect(jwtExamples.length).toBeGreaterThan(0);
const detected = jwtExamples.filter(s => isDetected(s, detector));
expect(detected.length / jwtExamples.length).toBeGreaterThan(0.7);
});
it('should detect Slack tokens', () => {
const slackExamples = corpus.filter(s =>
s.category === 'slack-tokens' && (s.value.startsWith('xox') || s.value.includes('hooks.slack.com'))
);
expect(slackExamples.length).toBeGreaterThan(0);
const detected = slackExamples.filter(s => isDetected(s, detector));
expect(detected.length / slackExamples.length).toBeGreaterThan(0.7);
});
it('should detect database connection strings', () => {
const dbExamples = corpus.filter(s =>
s.category === 'database-urls' &&
(s.value.startsWith('postgres') || s.value.startsWith('mysql') ||
s.value.startsWith('mongodb') || s.value.startsWith('redis'))
);
expect(dbExamples.length).toBeGreaterThan(0);
const detected = dbExamples.filter(s => isDetected(s, detector));
expect(detected.length / dbExamples.length).toBeGreaterThan(0.7);
});
it('should detect SSH keys via entropy', () => {
// SSH keys are parsed as individual lines of base64 content
const sshExamples = corpus.filter(s =>
s.category === 'ssh-keys' &&
(s.value.length > 20 || s.value.includes('fake@example.com'))
);
expect(sshExamples.length).toBeGreaterThan(0);
const detected = sshExamples.filter(s => isDetected(s, detector));
expect(detected.length / sshExamples.length).toBeGreaterThan(0.8);
});
});
describe('Individual Secret Validation', () => {
it('provides detailed detection info for each category', () => {
const summary: Record<string, { tested: number; detected: number; details: string[] }> = {};
for (const secret of corpus) {
if (!summary[secret.category]) {
summary[secret.category] = { tested: 0, detected: 0, details: [] };
}
summary[secret.category].tested++;
const detected = isDetected(secret, detector);
if (detected) {
summary[secret.category].detected++;
}
// Add detail for first 2 of each category
if (summary[secret.category].details.length < 2) {
const details = getDetectionDetails(secret, detector);
const status = detected ? '✓' : '✗';
summary[secret.category].details.push(
`${status} "${secret.value.substring(0, 30)}..." → ${detected ? details.map(d => d.pattern.name).join(', ') : 'NOT DETECTED'}`
);
}
}
// Log the detailed summary
console.log('\n=== Detailed Detection Summary ===\n');
for (const [category, data] of Object.entries(summary)) {
const accuracy = ((data.detected / data.tested) * 100).toFixed(1);
console.log(`${category}: ${data.detected}/${data.tested} (${accuracy}%)`);
data.details.forEach(d => console.log(` ${d}`));
console.log('');
}
});
});
});
// Export for use in other tests
export { loadSecretCorpus, isDetected, getDetectionDetails };
export type { SecretExample, CorpusResults, CategoryResult };
+66
View File
@@ -0,0 +1,66 @@
# AWS Access Keys - Realistic Format (All Revoked/Expired/Fake)
# Format: AKIA[0-9A-Z]{16} (20 characters total, starting with AKIA)
# Source: AWS Documentation Examples, TruffleHog Test Data
# Example 1: Standard AWS Access Key ID (from AWS docs)
AKIAIOSFODNN7EXAMPLE
# Example 2: Another AWS Access Key ID
AKIAI44QH8DHBEXAMPLE
# Example 3: AWS Access Key with numeric suffix
AKIA1234567890ABCDEF
# Example 4: Development account key (fake)
AKIADEVELOPER1234567
# Example 5: Test environment key (fake)
AKIATESTENVIRONMENT8
# Example 6: Production placeholder (fake)
AKIAPRODUCTIONENV123
# Example 7: CI/CD pipeline key (fake/revoked)
AKIACICDPIPELINE1234
# Example 8: Backup service key (fake)
AKIABACKUPSERVICE987
# Example 9: Monitoring key (fake)
AKIAMONITORINGTOOL56
# Example 10: Logging service key (fake)
AKIALOGGINGSERVICE78
# Example 11: Notification service key (fake)
AKIANOTIFICATIONSVC9
# Example 12: Analytics service key (fake)
AKIAANALYTICSSERVC01
# Example 13: Storage service key (fake)
AKIASTORAGESERVICE02
# Example 14: Compute service key (fake)
AKIACOMPUTESERVICE03
# Example 15: Database service key (fake)
AKIADATABASESERV04
# AWS Secret Access Keys (40-character base64-like strings)
# These are FAKE/EXAMPLE keys only
# Example Secret Key 1 (from AWS docs)
wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Example Secret Key 2
xJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Example Secret Key 3 (fake)
aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3
# Example Secret Key 4 (fake)
zY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA7
# Example Secret Key 5 (fake)
AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCd
+38
View File
@@ -0,0 +1,38 @@
# Database Connection Strings - Realistic Format (All Fake Credentials)
# Source: Various Database Documentation, Connection String Patterns
# PostgreSQL Connection Strings
postgres://user:password123@localhost:5432/mydb
postgresql://admin:secret456@db.example.com:5432/production
postgres://app_user:app_pass_789@postgres.internal:5432/app_db?sslmode=require
postgres://readonly:ro_pass_123@replica.postgres.com:5432/analytics
postgres://migration:migrate_456@primary.db.internal:5432/main
# MySQL Connection Strings
mysql://user:password@localhost:3306/database
mysql://admin:admin123@mysql.example.com:3306/production_db
mysql://app:app_secret@db.internal:3306/app_database?charset=utf8mb4
mysql://readonly:ro_password@replica.mysql.com:3306/analytics_db
mysql://backup:backup_pass@backup.mysql.internal:3306/backup_db
# MongoDB Connection Strings
mongodb://user:password@localhost:27017/mydb
mongodb://admin:admin123@mongo.example.com:27017/production?authSource=admin
mongodb+srv://app_user:app_pass@cluster.mongodb.net/app_database?retryWrites=true
mongodb://readonly:ro_pass@replica.mongodb.com:27017/analytics
mongodb://backup:backup123@backup.mongo.internal:27017/backup_db
# Redis Connection Strings
redis://:password123@localhost:6379/0
redis://:redis_pass_456@redis.example.com:6379/0
redis://app:app_redis_pass@redis.internal:6379/1
redis://localhost:6379/0 (no auth - development only)
redis://:complex_redis_password_789@sentinel.redis.internal:26379
# Amazon RDS Connection Strings
postgres://dbadmin:dbadmin123@mydb.abc123xyz.us-east-1.rds.amazonaws.com:5432/mydb
mysql://admin:rds_password@prod-mysql.abc123xyz.us-west-2.rds.amazonaws.com:3306/production
# Connection strings with special characters (URL encoded)
postgres://user:p%40ssw%23rd%21@localhost:5432/mydb
mysql://admin:pass%26word%3D123@localhost:3306/db
+55
View File
@@ -0,0 +1,55 @@
# Generic API Keys - Realistic Format (All Fake/Example Keys)
# Source: Common API Key Patterns from GitHub Secret Scanning
# Standard API Key patterns
api_key=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
api_key: 1234567890abcdef1234567890abcdef
api-key=xw9v8u7t6s5r4q3p2o1n0m9l8k7j6i5h4g3f2e1d0c
apikey=AbCdEfGhIjKlMnOpQrStUvWxYz123456
API_KEY=0123456789ABCDEF0123456789ABCDEF
# Service-specific API key patterns
service_api_key=sk_1234567890abcdefghijklmnopqrstuv
internal_api_key=ik_abcdefghijklmnopqrstuvwxyz123456
external_api_key=ek_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
client_api_key=ck_0123456789abcdefghijklmnopqrstuvwx
# API Keys with prefixes
key_live_abcdefghijklmnopqrstuvwxyz123456
key_test_abcdefghijklmnopqrstuvwxyz123456
key_prod_abcdefghijklmnopqrstuvwxyz1234567890
key_dev_abcdefghijklmnopqrstuvwxyz1234567890
# API Keys in headers/authorization
Authorization: ApiKey abcdefghijklmnopqrstuvwxyz123456
X-API-Key: 0123456789abcdef0123456789abcdef
X-Api-Key: ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
Api-Key: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# Random API key formats (common patterns)
abcdefghijklmnopqrstuvwxyz1234567890abcd
1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234
abcdef1234567890abcdef1234567890abcdef12
0123456789abcdefghijklmnopqrstuvwxyz0123
# API keys with version prefixes
v1_key_abcdefghijklmnopqrstuvwxyz12345678
v2_api_key_abcdefghijklmnopqrstuvwxyz12345
api_v3_key_abcdefghijklmnopqrstuvwxyz1234
# Integration API keys
integration_key_int_abcdefghijklmnopqrstuvw
webhook_key_whk_abcdefghijklmnopqrstuvwxyz
partner_api_key_part_abcdefghijklmnopqrst
# Environment-specific API keys
prod_api_key_prod_abcdefghijklmnopqrstuvw1
dev_api_key_dev_abcdefghijklmnopqrstuvwxyz
staging_api_key_stg_abcdefghijklmnopqrstuv
test_api_key_test_abcdefghijklmnopqrstuvwx
# SaaS platform API key patterns
algolia_api_key=abcdefghijklmnopqrstuvwxyz1234567890abcdef
sendgrid_api_key=SG.abcdefghijklmnopqrstuvwxyz1234567890.ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
mailgun_api_key=key-abcdefghijklmnopqrstuvwxyz1234567890abcdef
Twilio_api_key=SKabcdefghijklmnopqrstuvwxyz1234567890abcdef12
+43
View File
@@ -0,0 +1,43 @@
# GitHub Tokens - Realistic Format (All Revoked/Expired/Fake)
# Format: ghp_[a-zA-Z0-9]{36} (40 characters total)
# Source: GitHub Secret Scanning Patterns, GitHub Docs
# Personal Access Tokens (classic)
ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0000
ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1111
ghp_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2222
ghp_cccccccccccccccccccccccccccccccccccc3333
ghp_dddddddddddddddddddddddddddddddddddd4444
# Fine-grained Personal Access Tokens
ghp_efgh1234efgh5678efgh9012efgh3456efgh7890
ghp_ijkl5678ijkl9012ijkl3456ijkl7890ijkl1234
ghp_mnop9012mnop3456mnop7890mnop1234mnop5678
ghp_qrst3456qrst7890qrst1234qrst5678qrst9012
# OAuth Access Tokens
gho_wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww5555
gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6666
gho_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy7777
gho_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz8888
# GitHub App Installation Access Tokens
ghs_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9999
ghs_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000
ghs_cccccccccccccccccccccccccccccccccccc1111
# GitHub App User Access Tokens
ghu_dddddddddddddddddddddddddddddddddddd2222
ghu_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3333
# GitHub App Refresh Tokens
ghr_ffffffffffffffffffffffffffffffffffff4444
ghr_gggggggggggggggggggggggggggggggggggg5555
# GitHub App User-to-Server Tokens
ghu_hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh6666
ghu_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii7777
# Server-to-Server Tokens
ghs_jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj8888
ghs_kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk9999
+33
View File
@@ -0,0 +1,33 @@
# JWT Tokens - Realistic Format (All Revoked/Expired/Fake)
# Format: eyJ[header].eyJ[payload].[signature] (base64url encoded)
# Source: JWT.io Examples, RFC 7519
# Example 1: Standard JWT with HS256 (from jwt.io)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Example 2: JWT with RS256
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyMTIzIiwiYXVkIjoiYXBwNDU2IiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTYyNDI2MjJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# Example 3: JWT with minimal payload
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.hB4eJ1Q2k3l4m5n6o7p8q9r0s1t2u3v4w5x6y7z8
# Example 4: JWT with ES256
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNDU2IiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.MEQCIH8w7fKj4K0vLK7W1fQ8ZzY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA
# Example 5: JWT with EdDSA
eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbmlzdHJhdG9yIn0.6ICJmM2NhYjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg
# Example 6: API Gateway JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJhcGktY2xpZW50LTEyMyIsInNjb3BlIjoicmVhZCB3cml0ZSIsImV4cCI6MTY0MTIzNDU2N30.signaturepart123456789
# Example 7: Auth0-style JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ25pbmcta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDEyMzQ1Njc4OSIsImF1ZCI6Im15LWFwaSIsImlhdCI6MTY0NjEyMzQ1NiwiZXhwIjoxNjQ2MTIzNDU2fQ.signaturepart987654321
# Example 8: Firebase JWT
eyJhbGciOiJSUzI1NiIsImtpZCI6ImZpcmViYXNlLWtleS0xIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcHJvamVjdC0xMjMiLCJhdWQiOiJwcm9qZWN0LTEyMyIsImF1dGhfdGltZSI6MTY0NjEyMzQ1NiwidXNlcl9pZCI6InRlc3R1c2VyMTIzIiwic3ViIjoidGVzdHVzZXIxMjMiLCJpYXQiOjE2NDYxMjM0NTZ9.signaturepart567890123
# Example 9: AWS Cognito JWT
eyJraWQiOiJjb2duaXRvLWtleS0xIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIxMjM0NTY3OC05MGFiLWNkZWYtMTIzNC01Njc4LTkwYWJjZGVmZ2hpIiwiY29nbml0bzpncm91cHMiOlsiYWRtaW5zIl0sImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXhhbXBsZS5jb20iLCJjbGllbnRfaWQiOiIxMjM0NTY3ODkwYWJjIiwib3JpZ2luX2p0aSI6ImFiY2QtMTIzNCIsImV2ZW50X2lkIjoiZWZkZS0xMjM0IiwidG9rZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJhd3MuY29nbml0by5zaWduaW4udXNlci5hZG1pbiIsImF1dGhfdGltZSI6MTY0NjEyMzQ1NiwiZXhwIjoxNjQ2MTIzNDU2LCJpYXQiOjE2NDYxMjM0NTYsImp0aSI6ImFiY2QtMTIzNCIsInVzZXJuYW1lIjoidGVzdHVzZXIifQ.signaturepart234567890
# Example 10: Okta JWT
eyJhbGciOiJSUzI1NiIsImtpZCI6Im9rdGEta2V5LTEifQ.eyJzdWIiOiIwMHUxYTU0OGM0eG42VFkzNDU2NyIsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJ2ZXIiOjEsImlzcyI6Imh0dHBzOi8vZXhhbXBsZS1vay10YS5va3RhLmNvbSIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2NDYxMjM0NTYsImV4cCI6MTY0NjEyMzQ1NiwianRpIjoiSUQuMTIzNDU2Nzg5MCIsImFtciI6WyJwYXNzd29yZCJdLCJpZHAiOiIwMG9hYmNkZWZnaCIsImF1dGhfdGltZSI6MTY0NjEyMzQ1NiwiYXRfaGFzaCI6ImFiY2RlZmcxMjM0NTYifQ.signaturepart890123456
+40
View File
@@ -0,0 +1,40 @@
# OAuth Tokens - Realistic Format (All Revoked/Expired/Fake)
# Source: OAuth 2.0 RFC 6749, Provider Documentation
# OAuth 2.0 Access Tokens (various formats)
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
ya29.a0Aa4b16C3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4y5Z6a7B8c9
EAANlZA2X7ZCZAYXABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghij
# OAuth 2.0 Refresh Tokens
1/aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefABCDEFGHIJKLMNO
refresh_token_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
RtgDeFCjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYz12345678
# Authorization Codes (short-lived)
4/aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdef-ABCDEFGHIJKLMNO
auth_code_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4
AQCDEFGHIJKLMN0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZ123456
# PKCE Code Verifiers (used with OAuth 2.0 PKCE)
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefABCDEFGH
M7Qd2N6aPl4mRn8kTp2vWx5yZb3cEf6hAj9kLm1nOp4qRs7tUv0wXy2z5
# Device Codes (OAuth 2.0 Device Flow)
GAQDEFGHIJKLMN0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZ123456
AAQDEFGHIJKLMN0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZ123456
# ID Tokens (OpenID Connect JWTs)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMjM0NTY3ODkwMTIzLTBhYjJjZDNlZi1naGk0a2w1bW42b3BxcnN0dXYud2FwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTIzNDU2Nzg5MDEyMy0wYWJjZDNlZi1naGk0a2w1bW42b3BxcnN0dXYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDc1ODU0NDE2OTU4ODU5NzkwMzUiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJYQmlUMWx4dU9BWWpJZGVaN0dPQl9BIiwiaWF0IjoxNjQ2MTIzNDU2LCJleHAiOjE2NDYxMjM0NTZ9.signature
# Client Credentials (Client ID and Secret pairs)
client_id: abcdefgh12345678.apps.example.com
client_secret: GOCSPX-abcdefghijklmnopqrstuvwxyz1234567890abcdef
# Alternative OAuth token formats
Slack: xoxe.xoxp-1-aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdef
Shopify: shpat_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Discord: MzA4NjE4NDE1MzQzNjM1MzYw.DaScqQ.aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdef
Twitch: oauth:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
+29
View File
@@ -0,0 +1,29 @@
# Slack Tokens - Realistic Format (All Revoked/Expired/Fake)
# Format: xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}
# Source: Slack API Documentation, Slack Secret Scanning
# Bot Tokens (xoxb-) - Used by bots to connect to Slack
xoxb-1234567890123-1234567890123-aBcDeFgHiJkLmNoPqRsTuVwX
xoxb-4567890123456-4567890123456-bCdEfGhIjKlMnOpQrStUvWxY
xoxb-7890123456789-7890123456789-cDeFgHiJkLmNoPqRsTuVwXyZ
xoxb-0123456789012-0123456789012-dEfGhIjKlMnOpQrStUvWxYz0
xoxb-2345678901234-2345678901234-eFgHiJkLmNoPqRsTuVwXyZ01
# User Tokens (xoxp-) - OAuth user access tokens
xoxp-1234567890123-1234567890123-1234567890123-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
xoxp-4567890123456-4567890123456-4567890123456-b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7
xoxp-7890123456789-7890123456789-7890123456789-c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8
# App Tokens (xapp-) - Slack app-level tokens
xapp-1-A1234567890-1234567890123-aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGh
xapp-1-B2345678901-2345678901234-bCdEfGhIjKlMnOpQrStUvWxYz0123456789BcDeFgHi
xapp-1-C3456789012-3456789012345-cDeFgHiJkLmNoPqRsTuVwXyZ012345678901CdEfGhIj
# Refresh Tokens (xoxr-) - OAuth refresh tokens
xoxr-1234567890123-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
xoxr-4567890123456-b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7
# Webhook URLs (not tokens but contain secrets)
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
https://hooks.slack.com/services/T12345678/B12345678/AbCdEfGhIjKlMnOpQrStUvWx
https://hooks.slack.com/services/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
+55
View File
@@ -0,0 +1,55 @@
# SSH Keys - Realistic Format (All Fake/Example Keys)
# Source: SSH Key Documentation, OpenSSH Formats
# RSA Private Key (2048-bit example - FAKE)
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MhgwMbRvI0MBZhpJ
qX3YBb+QXBU0yI0g1xN0rG2XcfJtK5Xy4qKWPJbE+esL4WzGzQnrdBkYy+IZmMbR
y5PG0/VbKNdrnZBRtWvJtLGJ7fFLb0vKELdI+YKA5QqHI9+lEhKQ/JYQ8GGDXV0c
sIQNx0TNVL8Y7xEqzFKKqJ4B0MPLoMOcT7Q3t3LZwOVTrQHK2p8vVwXfR2LWBd8z
PsKqSJQOdVsJI6FNiRZrGqmKCbBPKBf3jVc3jE7QqKWNZkLKE5b4ANPmHMq5CvXr
QGU1lGXN2Tq7x8mFhBDb8T0wPBmQq0QGR1QLQeZG0QIDAQABAoIBADY8C0E1CQGn
o7P8T3wVnQE1KzFVB4k4rO8LqEPLhD9F4fFSB0KY8bUHZ3PCg6P5YQYsWE4xXkJf
Hh9XmB0Yt3VCCCN0S1qIQXo5Yd8QKc5J7j3Nh4mT0fVL5y0jK1ZJ0BC5Bn8P8Qf
HQIIQVIxNDEUhT0sEREREBMTExQVFRUWFxcXGBgYGRkZGhoaGxsbHBwcHR0dHh4e
Hx8fICAgISEhIiIiIyMjJCUlJSYmJicoKCkqKiorKyssLC0tLS4uLi8vLzAxMTIz
MzQ0NTU2Njc3ODg5OTpCQkNDRERFRUZGR0dHSEhJSUpKS0tMTU1OTk9PUFFQUVFR
UlJTU1RUVVVWVldXV1hYWFlZWVpaW1tbXFxcXV1eXl9fYGFiYmNkZWZnaGlqa2xt
bm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5yd
np+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zN
zs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9
/v8=
-----END RSA PRIVATE KEY-----
# OpenSSH format ED25519 Private Key (FAKE)
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB6NqFGpMTjJ7dZzymp9zknBfzKnkzIfzZnZp7rXGK6uQAAAJhJlaKUWZWi
lAAAAAtzc2gtZWQyNTUxOQAAACB6NqFGpMTjJ7dZzymp9zknBfzKnkzIfzZnZp7rXGK6uQ
AAAEA6PbHhnp6q+FzU2nRr0q3bPsykZ7TzNXbC5IMeCWzNdXo2oUakxOMnt1nPKan3OS
MF/MqeTMh/Nmdmnu9cYrq5AAAADmZha2VAZXhhbXBsZS5jb20=
-----END OPENSSH PRIVATE KEY-----
# EC (Elliptic Curve) Private Key (FAKE)
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBv9b1D8mT8qC8p4a6Z7F8VvG7bE2Y9C5L8U5sC9oBSAU6gsjjsa9xCBoAU
BgUrgQQAIgYgMIGABgorBgEEAZdYAALBMDswOQIBAQQfMB0GByqGSM49AQECEC0r
N0ZpH8Kvn7mMBQYHZW4yY4XJJjAkAgEBBByB3vGJpQrmp3RqNRuS9BAAxj7IWdK2
bG0LmZo5QKQ=
-----END EC PRIVATE KEY-----
# DSA Private Key (FAKE - legacy format)
-----BEGIN DSA PRIVATE KEY-----
MIIDTwIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MhgwMbRvI0MBZhpJ
qX3YBb+QXBU0yI0g1xN0rG2XcfJtK5Xy4qKWPJbE+esL4WzGzQnrdBkYy+IZmMbR
y5PG0/VbKNdrnZBRtWvJtLGJ7fFLb0vKELdI+YKA5QqHI9+lEhKQ/JYQ8GGDXV0c
sIQNx0TNVL8Y7xEqzFKKqJ4B0MPLoMOcT7Q3t3LZwOVTrQHK2p8vVwXfR2LWBd8z
PsKqSJQOdVsJI6FNiRZrGqmKCbBPKBf3jVc3jE7QqKWNZkLKE5b4ANPmHMq5CvXr
QGU1lGXN2Tq7x8mFhBDb8T0wPBmQq0QGR1QLQeZG0QIVALznc8aJLvBbfnzF3G0O
L8JbOg2xAoGBALr5OBb3TWm2h6FZD5K3z3fNd9kmQqKmxnmnZdJjkpUfFbXOuKZp
BzLm0g9zH7W8PMRz7r8zEeZ4s6dW9CjD+M5T8nKQw2h5b7Eq7cQbGa4iNfRmU0pL
QvLzP7LZvE9CY7dYxFQAQ9DqAzJLBQKvFYzK8Q7mNgUYeE5CpZcMGRG1FLyZG0Q
-----END DSA PRIVATE KEY-----
# SSH Public Key (RSA - safe to share but included for completeness)
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MhgwMbRvI0MBZhpJqX3YBb+QXBU0yI0g1xN0rG2XcfJtK5Xy4qKWPJbE+esL4WzGzQnrdBkYy+IZmMbRy5PG0/VbKNdrnZBRtWvJtLGJ7fFLb0vKELdI+YKA5QqHI9+lEhKQ/JYQ8GGDXV0csIQNx0TNVL8Y7xEqzFKKqJ4B0MPLoMOcT7Q3t3LZwOVTrQHK2p8vVwXfR2LWBd8zPsKqSJQOdVsJI6FNiRZrGqmKCbBPKBf3jVc3jE7QqKWNZkLKE5b4ANPmHMq5CvXrQGU1lGXN2Tq7x8mFhBDb8T0wPBmQq0QGR1QLQeZG0Q= fake@example.com
+36
View File
@@ -0,0 +1,36 @@
# Stripe API Keys - Realistic Format (All Revoked/Expired/Fake)
# Format: sk_live_[0-9a-zA-Z]{24,} or sk_test_[0-9a-zA-Z]{24,}
# Source: Stripe API Documentation, Stripe Secret Scanning
# Live Secret Keys (sk_live_) - REVOKED/FAKE ONLY
sk_live_abcdefghijklmnopqrstuvwxyz123456
sk_live_51abcdefghijklmnopqrstuvwx123456789
sk_live_abcdefghijklmnopqrstuvwxyzABCDEFG
sk_live_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
sk_live_0123456789abcdefghijklmnopqrstuv
# Test Secret Keys (sk_test_)
sk_test_abcdefghijklmnopqrstuvwxyz123456
sk_test_51abcdefghijklmnopqrstuvwx123456789
sk_test_abcdefghijklmnopqrstuvwxyzABCDEFG
sk_test_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
sk_test_0123456789abcdefghijklmnopqrstuv
# Live Publishable Keys (pk_live_) - Safe to share but included for completeness
pk_live_abcdefghijklmnopqrstuvwxyz123456
pk_live_51abcdefghijklmnopqrstuvwx123456789
# Test Publishable Keys (pk_test_)
pk_test_abcdefghijklmnopqrstuvwxyz123456
pk_test_51abcdefghijklmnopqrstuvwx123456789
# Restricted API Keys (rk_live_ and rk_test_)
rk_live_abcdefghijklmnopqrstuvwxyz1234567890
rk_test_abcdefghijklmnopqrstuvwxyz1234567890
# Stripe Connect Keys
sk_live_acct_1234567890abcdefghijklmnopqrstuvwxyz1234567890
# Webhook Endpoint Secrets (whsec_)
whsec_abcdefghijklmnopqrstuvwxyz1234567890abcdef
whsec_1234567890abcdefghijklmnopqrstuvwxyz123456
+418
View File
@@ -0,0 +1,418 @@
/**
* Corpus Validation Script
*
* Validates all secret detection patterns against the realistic corpus of 146 examples.
* Goal: Achieve 85%+ detection rate with <5% false positives.
*/
import { V2_PATTERNS, V2_PATTERN_COUNTS } from '../../../src/patterns/v2/index.js';
import type { SecretPattern } from '../../../src/types.js';
import { readFileSync } from 'fs';
import { resolve } from 'path';
// ============================================================================
// CORPUS FILE DEFINITIONS
// ============================================================================
const CORPUS_FILES = [
{ file: 'aws-keys.txt', expectedPatterns: ['aws_access_key_id', 'aws_secret_access_key'], totalExamples: 20 },
{ file: 'github-tokens.txt', expectedPatterns: ['github_personal_access_token', 'github_oauth_token', 'github_app_token', 'github_user_token', 'github_refresh_token'], totalExamples: 19 },
{ file: 'slack-tokens.txt', expectedPatterns: ['slack_bot_token', 'slack_user_token', 'slack_app_token', 'slack_webhook_url'], totalExamples: 11 },
{ file: 'database-urls.txt', expectedPatterns: ['postgres_connection_string', 'mysql_connection_string', 'mongodb_connection_string', 'redis_connection_string'], totalExamples: 18 },
{ file: 'jwt-tokens.txt', expectedPatterns: ['jwt_token_standard', 'jwt_token_hs256', 'jwt_token_rs256', 'jwt_token_es256'], totalExamples: 10 },
{ file: 'oauth-tokens.txt', expectedPatterns: ['oauth_access_token', 'oauth_refresh_token', 'oauth_authorization_code', 'gcp_oauth_access_token'], totalExamples: 15 },
{ file: 'ssh-keys.txt', expectedPatterns: ['ssh_rsa_private_key', 'ssh_openssh_private_key', 'ssh_ecdsa_private_key', 'ssh_dsa_private_key'], totalExamples: 5 },
{ file: 'stripe-keys.txt', expectedPatterns: ['stripe_live_secret_key', 'stripe_test_secret_key', 'stripe_webhook_secret'], totalExamples: 12 },
{ file: 'generic-api-keys.txt', expectedPatterns: ['generic_api_key_header', 'generic_api_key_pattern', 'generic_secret_assignment'], totalExamples: 27 },
];
const CORPUS_DIR = resolve(process.cwd(), 'test/fixtures/realistic-secrets');
// ============================================================================
// TYPES
// ============================================================================
interface DetectionResult {
line: string;
lineNumber: number;
detected: boolean;
matchedBy: string[];
isComment: boolean;
isEmpty: boolean;
}
interface FileValidationResult {
fileName: string;
totalLines: number;
secretsFound: number;
falsePositives: number;
detectionRate: number;
falsePositiveRate: number;
detections: DetectionResult[];
}
interface OverallResult {
totalExamples: number;
totalDetected: number;
totalMissed: number;
totalFalsePositives: number;
detectionRate: number;
falsePositiveRate: number;
fileResults: FileValidationResult[];
patternStats: Map<string, { detected: number; total: number }>;
}
// ============================================================================
// PATTERN TESTING
// ============================================================================
/**
* Test a single line against all patterns
*/
function testLineAgainstPatterns(line: string, lineNumber: number, patterns: SecretPattern[]): DetectionResult {
const trimmed = line.trim();
const isComment = trimmed.startsWith('#');
const isEmpty = trimmed.length === 0;
// Skip comment and empty lines for detection metrics, but track them
const matchedPatterns: string[] = [];
for (const pattern of patterns) {
try {
const regex = new RegExp(pattern.regex.source, pattern.regex.flags.includes('g') ? pattern.regex.flags : pattern.regex.flags + 'g');
if (regex.test(line)) {
matchedPatterns.push(pattern.name);
}
} catch (e) {
console.error(` ⚠️ Pattern error in ${pattern.name}: ${e}`);
}
}
return {
line: line.substring(0, 80) + (line.length > 80 ? '...' : ''),
lineNumber,
detected: matchedPatterns.length > 0,
matchedBy: matchedPatterns,
isComment,
isEmpty,
};
}
/**
* Determine if a detection is a likely false positive
*/
function isLikelyFalsePositive(detection: DetectionResult, fileName: string): boolean {
// Comment lines are not false positives if they're examples
if (detection.isComment && !detection.line.includes('Example')) {
return false; // Comments explaining patterns are fine
}
// If it's a comment but we detected something, might be a false positive
if (detection.isComment && detection.detected) {
// Check if it looks like a real secret in the comment
const hasSecretIndicators = /[a-zA-Z0-9]{16,}/.test(detection.line);
if (!hasSecretIndicators) {
return true;
}
}
return false;
}
/**
* Validate a single corpus file
*/
function validateCorpusFile(fileName: string, expectedPatterns: string[], totalExamples: number): FileValidationResult {
const filePath = resolve(CORPUS_DIR, fileName);
const content = readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
const detections: DetectionResult[] = [];
let secretsFound = 0;
let falsePositives = 0;
for (let i = 0; i < lines.length; i++) {
const detection = testLineAgainstPatterns(lines[i], i + 1, V2_PATTERNS);
detections.push(detection);
if (detection.detected && !detection.isComment && !detection.isEmpty) {
secretsFound++;
} else if (detection.detected && isLikelyFalsePositive(detection, fileName)) {
falsePositives++;
}
}
// Calculate meaningful lines (non-comment, non-empty)
const meaningfulLines = detections.filter(d => !d.isComment && !d.isEmpty).length;
const detectionRate = meaningfulLines > 0 ? (secretsFound / meaningfulLines) * 100 : 0;
const falsePositiveRate = secretsFound > 0 ? (falsePositives / secretsFound) * 100 : 0;
return {
fileName,
totalLines: lines.length,
secretsFound,
falsePositives,
detectionRate,
falsePositiveRate,
detections,
};
}
/**
* Run full corpus validation
*/
function runValidation(): OverallResult {
console.log('╔══════════════════════════════════════════════════════════════╗');
console.log('║ OpenCode Filter V2 - Corpus Validation ║');
console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`Pattern Counts by Category:`);
Object.entries(V2_PATTERN_COUNTS).forEach(([cat, count]) => {
console.log(`${cat}: ${count} patterns`);
});
console.log('');
const fileResults: FileValidationResult[] = [];
const patternStats = new Map<string, { detected: number; total: number }>();
for (const corpus of CORPUS_FILES) {
console.log(`📁 Validating: ${corpus.file}`);
console.log(` Expected patterns: ${corpus.expectedPatterns.join(', ')}`);
const result = validateCorpusFile(corpus.file, corpus.expectedPatterns, corpus.totalExamples);
fileResults.push(result);
console.log(` Total lines: ${result.totalLines}`);
console.log(` Secrets detected: ${result.secretsFound}`);
console.log(` Detection rate: ${result.detectionRate.toFixed(1)}%`);
console.log('');
// Track pattern statistics
for (const detection of result.detections) {
if (detection.detected && !detection.isComment && !detection.isEmpty) {
for (const patternName of detection.matchedBy) {
const current = patternStats.get(patternName) || { detected: 0, total: 0 };
patternStats.set(patternName, { detected: current.detected + 1, total: current.total + 1 });
}
}
}
}
// Calculate overall metrics
let totalExamples = 0;
let totalDetected = 0;
let totalFalsePositives = 0;
for (const result of fileResults) {
const meaningfulLines = result.detections.filter(d => !d.isComment && !d.isEmpty).length;
totalExamples += meaningfulLines;
totalDetected += result.secretsFound;
totalFalsePositives += result.falsePositives;
}
const detectionRate = totalExamples > 0 ? (totalDetected / totalExamples) * 100 : 0;
const falsePositiveRate = totalDetected > 0 ? (totalFalsePositives / totalDetected) * 100 : 0;
return {
totalExamples,
totalDetected,
totalMissed: totalExamples - totalDetected,
totalFalsePositives,
detectionRate,
falsePositiveRate,
fileResults,
patternStats,
};
}
/**
* Print detailed report
*/
function printReport(result: OverallResult): void {
console.log('\n╔══════════════════════════════════════════════════════════════╗');
console.log('║ VALIDATION REPORT ║');
console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log('📊 OVERALL METRICS');
console.log('─────────────────────────────────────────────────────────────');
console.log(`Total Examples: ${result.totalExamples}`);
console.log(`Secrets Detected: ${result.totalDetected}`);
console.log(`Secrets Missed: ${result.totalMissed}`);
console.log(`False Positives: ${result.totalFalsePositives}`);
console.log('');
console.log(`Detection Rate: ${result.detectionRate.toFixed(1)}% ${result.detectionRate >= 85 ? '✅' : '❌ (< 85%)'}`);
console.log(`False Positive Rate: ${result.falsePositiveRate.toFixed(1)}% ${result.falsePositiveRate < 5 ? '✅' : '❌ (> 5%)'}`);
console.log('');
// File-by-file breakdown
console.log('📁 FILE-BY-FILE BREAKDOWN');
console.log('─────────────────────────────────────────────────────────────');
for (const fileResult of result.fileResults) {
const status = fileResult.detectionRate >= 80 ? '✅' : '⚠️ ';
console.log(`${status} ${fileResult.fileName}`);
console.log(` Lines: ${fileResult.totalLines} | Detected: ${fileResult.secretsFound} | Rate: ${fileResult.detectionRate.toFixed(1)}%`);
}
console.log('');
// Pattern effectiveness
console.log('🎯 TOP PATTERNS BY DETECTION');
console.log('─────────────────────────────────────────────────────────────');
const sortedPatterns = Array.from(result.patternStats.entries())
.sort((a, b) => b[1].detected - a[1].detected)
.slice(0, 15);
for (const [patternName, stats] of sortedPatterns) {
console.log(`${patternName}: ${stats.detected} detections`);
}
console.log('');
// Underperforming patterns (patterns that should match but didn't)
console.log('🔧 PATTERNS NEEDING ATTENTION');
console.log('─────────────────────────────────────────────────────────────');
for (const corpus of CORPUS_FILES) {
const fileResult = result.fileResults.find(r => r.fileName === corpus.file);
if (fileResult && fileResult.detectionRate < 80) {
console.log(`⚠️ ${corpus.file}: ${fileResult.detectionRate.toFixed(1)}% detection`);
console.log(` Expected patterns: ${corpus.expectedPatterns.join(', ')}`);
// Check which patterns are not matching
for (const expectedPattern of corpus.expectedPatterns) {
const stats = result.patternStats.get(expectedPattern);
if (!stats || stats.detected === 0) {
console.log(`${expectedPattern}: 0 detections - needs tuning`);
}
}
}
}
console.log('');
// Final verdict
console.log('🏁 FINAL VERDICT');
console.log('─────────────────────────────────────────────────────────────');
const passed = result.detectionRate >= 85 && result.falsePositiveRate < 5;
if (passed) {
console.log('✅ VALIDATION PASSED');
console.log(' Detection rate >= 85%: ✓');
console.log(' False positive rate < 5%: ✓');
} else {
console.log('❌ VALIDATION FAILED');
if (result.detectionRate < 85) {
console.log(` Detection rate too low: ${result.detectionRate.toFixed(1)}% (need >= 85%)`);
}
if (result.falsePositiveRate >= 5) {
console.log(` False positive rate too high: ${result.falsePositiveRate.toFixed(1)}% (need < 5%)`);
}
}
console.log('');
}
/**
* Run detailed line-by-line analysis for debugging
*/
function runDetailedAnalysis(fileName: string): void {
const corpus = CORPUS_FILES.find(c => c.file === fileName);
if (!corpus) {
console.log(`❌ Unknown corpus file: ${fileName}`);
return;
}
console.log(`\n🔍 Detailed Analysis: ${fileName}\n`);
const filePath = resolve(CORPUS_DIR, fileName);
const content = readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line.startsWith('#')) continue;
const detection = testLineAgainstPatterns(lines[i], i + 1, V2_PATTERNS);
if (detection.detected) {
console.log(`✅ Line ${i + 1}: DETECTED by [${detection.matchedBy.join(', ')}]`);
console.log(` ${line.substring(0, 60)}${line.length > 60 ? '...' : ''}`);
} else {
console.log(`❌ Line ${i + 1}: NOT DETECTED`);
console.log(` ${line.substring(0, 60)}${line.length > 60 ? '...' : ''}`);
}
}
}
// ============================================================================
// MAIN EXECUTION
// ============================================================================
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: bun run test/fixtures/realistic-secrets/validate-corpus.ts [options]
Options:
--help, -h Show this help message
--analyze <file> Run detailed analysis on a specific corpus file
--report Generate JSON report for CI/CD
Examples:
bun run test/fixtures/realistic-secrets/validate-corpus.ts
bun run test/fixtures/realistic-secrets/validate-corpus.ts --analyze aws-keys.txt
`);
process.exit(0);
}
if (args.includes('--analyze')) {
const fileIndex = args.indexOf('--analyze');
const fileName = args[fileIndex + 1];
if (fileName) {
runDetailedAnalysis(fileName);
} else {
console.log('❌ Please specify a file to analyze: --analyze <filename>');
process.exit(1);
}
} else {
const result = runValidation();
printReport(result);
// Save report
const reportPath = resolve(process.cwd(), '.sisyphus/evidence/v2-t10-corpus-validation.txt');
const reportContent = `
Corpus Validation Report
========================
Date: ${new Date().toISOString().split('T')[0]}
Total Examples: ${result.totalExamples}
Detected: ${result.totalDetected}
Missed: ${result.totalMissed}
Detection Rate: ${result.detectionRate.toFixed(1)}% ${result.detectionRate >= 85 ? '✅' : '❌'}
False Positives: ${result.totalFalsePositives}
False Positive Rate: ${result.falsePositiveRate.toFixed(1)}% ${result.falsePositiveRate < 5 ? '✅' : '❌'}
Top Patterns by Detection:
${Array.from(result.patternStats.entries())
.sort((a, b) => b[1].detected - a[1].detected)
.slice(0, 10)
.map(([name, stats]) => `- ${name}: ${stats.detected} detections`)
.join('\n')}
Files Needing Attention:
${result.fileResults
.filter(f => f.detectionRate < 80)
.map(f => `- ${f.fileName}: ${f.detectionRate.toFixed(1)}% detection`)
.join('\n') || 'None - all files meet targets'}
`;
try {
import('fs').then(fs => {
fs.mkdirSync(resolve(process.cwd(), '.sisyphus/evidence'), { recursive: true });
fs.writeFileSync(reportPath, reportContent);
console.log(`📄 Report saved to: ${reportPath}`);
});
} catch (e) {
// Ignore write errors
}
// Exit with appropriate code for CI/CD
const passed = result.detectionRate >= 85 && result.falsePositiveRate < 5;
process.exit(passed ? 0 : 1);
}