Skip to content

Commit b1e6c79

Browse files
committed
feat: add example key pattern detection (3 new patterns)
Add high-confidence detection for AI-generated example/placeholder API keys that traditional linters miss: - example-api-key: OpenAI keys with 'example' suffix (confidence 0.95) - example-github-pat: GitHub PATs with 'example' suffix (confidence 0.95) - placeholder-secret-basic: Obvious placeholder secrets (confidence 0.8) Also improve: - example-stripe-key: now covers pk_test_ and pk_live_ prefix formats - Split github-pat-general (general detection) from example-github-pat (example-specific, higher confidence) 8 new tests added, all 101 security tests passing. Refs: product-audit-5-scenarios.md Quick Wins - Example Key 模式检测
1 parent 29ee7cb commit b1e6c79

4 files changed

Lines changed: 274 additions & 3 deletions

File tree

packages/core/src/detectors/v4/security-pattern.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,15 @@ const SECURITY_PATTERNS: SecurityPattern[] = [
212212
// These patterns are commonly produced by AI from training data
213213
// and represent AI-specific security risks that traditional tools miss.
214214

215+
{
216+
id: 'example-api-key',
217+
pattern: /sk-(?:proj-|svcacct-)[a-zA-Z0-9]+-[a-zA-Z0-9]*example[a-zA-Z0-9]*/i,
218+
severity: 'error',
219+
confidence: 0.95,
220+
message: 'Possible example OpenAI API key detected. AI often copies documentation examples with placeholder values. Use environment variables instead.',
221+
languages: [],
222+
excludeContextPatterns: [/process\.env/i, /import\.meta\.env/i, /\bgetenv\b/i],
223+
},
215224
{
216225
id: 'example-openai-key',
217226
pattern: /sk-(?:proj-|svcacct-)[a-zA-Z0-9]{4,24}(-[a-zA-Z0-9]{4,24}){1,3}/,
@@ -229,6 +238,14 @@ const SECURITY_PATTERNS: SecurityPattern[] = [
229238
message: 'Placeholder secret value detected. AI often copies example values from documentation. Replace with proper secret management.',
230239
languages: [],
231240
},
241+
{
242+
id: 'placeholder-secret-basic',
243+
pattern: /(?:password|secret)\s*[:=]\s*['"][^'"]*(?:example|test|demo|sample)[^'"]*['"]/i,
244+
severity: 'warning',
245+
confidence: 0.8,
246+
message: 'Placeholder secret detected. AI commonly uses obvious placeholder values like "example" or "test" for secrets. Use environment variables instead.',
247+
languages: [],
248+
},
232249
{
233250
id: 'dynamic-cors-origin-reflection',
234251
pattern: /Access-Control-Allow-Origin.*?(?:req|request|headers\.origin|\$\{.*?origin)/i,
@@ -422,17 +439,25 @@ const SECURITY_PATTERNS: SecurityPattern[] = [
422439

423440
{
424441
id: 'example-stripe-key',
425-
pattern: /\bsk_(?:test|live)_[a-zA-Z0-9]{20,}/,
442+
pattern: /\b(?:pk|sk)_(?:test|live)_[a-zA-Z0-9]{20,}(?:example)?/i,
426443
severity: 'error',
427444
confidence: 0.9,
428445
message: 'Possible Stripe API key detected. AI often copies example keys from Stripe documentation. Use environment variables or a secrets manager.',
429446
languages: [],
430447
},
431448
{
432449
id: 'example-github-pat',
450+
pattern: /\bghp_[a-zA-Z0-9]{4,}example\b/i,
451+
severity: 'error',
452+
confidence: 0.95,
453+
message: 'Possible example GitHub Personal Access Token detected. AI often copies documentation examples with "example" suffix. Use environment variables instead.',
454+
languages: [],
455+
},
456+
{
457+
id: 'github-pat-general',
433458
pattern: /\bghp_[a-zA-Z0-9]{36}\b/,
434459
severity: 'error',
435-
confidence: 0.9,
460+
confidence: 0.8,
436461
message: 'Possible GitHub Personal Access Token detected. Verify this is not a real token and use environment variables.',
437462
languages: [],
438463
},
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { SecurityPatternDetector } from '../../src/detectors/v4/security-pattern.js';
2+
import type { DetectorContext } from '../../src/detectors/v4/types.js';
3+
import type { CodeUnit } from '../../src/ir/types.js';
4+
import { createCodeUnit } from '../../src/ir/types.js';
5+
6+
// Demo file with example/placeholder keys that AI commonly generates
7+
const demoSource = `// AI-generated code with example/placeholder keys
8+
import OpenAI from 'openai';
9+
10+
// Scenario 1: OpenAI example key (AI copied from docs)
11+
const openaiKey = "sk-proj-abc123-example";
12+
const openai = new OpenAI({ apiKey: openaiKey });
13+
14+
// Scenario 2: GitHub PAT with example suffix
15+
const githubToken = "ghp_aabbccddeeff00112233445566778899example";
16+
17+
// Scenario 3: Stripe secret key (from Stripe docs)
18+
const stripeSecretKey = "sk_test_abcdefghijklmnopqrstuvwxyzexample";
19+
20+
// Scenario 4: Placeholder secret value
21+
const config = {
22+
password: "example123",
23+
apiSecret: "changeme",
24+
authToken: "your_api_key_here",
25+
};
26+
27+
// Scenario 5: Environment variable with hardcoded fallback
28+
const dbPassword = process.env.DB_PASSWORD || "some-secret-value-here";
29+
30+
// This should NOT be flagged (proper env var usage)
31+
const productionKey = process.env.PRODUCTION_API_KEY;
32+
33+
async function main() {
34+
const completion = await openai.chat.completions.create({
35+
model: "gpt-4",
36+
messages: [{ role: "user", content: "Hello" }],
37+
});
38+
console.log(completion.choices[0].message);
39+
}
40+
41+
main();`;
42+
43+
function makeUnit(source: string): CodeUnit {
44+
return createCodeUnit({
45+
id: 'func:demo.ts:main',
46+
file: 'demo.ts',
47+
language: 'typescript',
48+
kind: 'function',
49+
location: { startLine: 0, startColumn: 0, endLine: source.split('\n').length, endColumn: 0 },
50+
source,
51+
});
52+
}
53+
54+
const detector = new SecurityPatternDetector();
55+
const context: DetectorContext = { projectRoot: '/project', allFiles: ['demo.ts'] };
56+
57+
async function run() {
58+
const unit = makeUnit(demoSource);
59+
const results = await detector.detect([unit], context);
60+
61+
console.log('=== Example Key Detection Demo Report ===\n');
62+
console.log(`File: demo.ts`);
63+
console.log(`Total detections: ${results.length}\n`);
64+
65+
for (const result of results) {
66+
console.log(`[${result.severity.toUpperCase()}] ${result.message}`);
67+
console.log(` Line: ${result.line}`);
68+
console.log(` Pattern: ${result.metadata.patternId}`);
69+
console.log(` Confidence: ${result.confidence}`);
70+
console.log(` Matched: ${result.metadata.matchedLine}`);
71+
console.log();
72+
}
73+
74+
// Summary
75+
const newPatterns = ['example-api-key', 'example-github-pat', 'placeholder-secret-basic'];
76+
const newDetections = results.filter(r => newPatterns.includes(r.metadata.patternId));
77+
78+
console.log('=== New Pattern Detections (this update) ===');
79+
console.log(`Count: ${newDetections.length}`);
80+
for (const result of newDetections) {
81+
console.log(` - ${result.metadata.patternId}: Line ${result.line}`);
82+
}
83+
}
84+
85+
run().catch(console.error);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Example Key Pattern Detection Tests
3+
*
4+
* Tests for detecting example/placeholder keys that AI often
5+
* copies from documentation but accidentally leaves in code.
6+
*
7+
* @since 0.5.0
8+
*/
9+
10+
import { describe, it, expect } from 'vitest';
11+
import { SecurityPatternDetector } from '../../src/detectors/v4/security-pattern.js';
12+
import type { DetectorContext } from '../../src/detectors/v4/types.js';
13+
import type { CodeUnit } from '../../src/ir/types.js';
14+
import { createCodeUnit } from '../../src/ir/types.js';
15+
16+
// ─── Helpers ───────────────────────────────────────────────────────
17+
18+
function makeUnit(
19+
source: string,
20+
language: CodeUnit['language'] = 'typescript',
21+
file: string = 'test.ts',
22+
): CodeUnit {
23+
return createCodeUnit({
24+
id: `func:${file}:fn`,
25+
file,
26+
language,
27+
kind: 'function',
28+
location: { startLine: 0, startColumn: 0, endLine: source.split('\n').length, endColumn: 0 },
29+
source,
30+
});
31+
}
32+
33+
function createContext(): DetectorContext {
34+
return {
35+
projectRoot: '/project',
36+
allFiles: ['test.ts'],
37+
};
38+
}
39+
40+
// ─── Tests ─────────────────────────────────────────────────────────
41+
42+
describe('Example Key Pattern Detection', () => {
43+
const detector = new SecurityPatternDetector();
44+
45+
it('should detect OpenAI example keys with example suffix', async () => {
46+
const unit = makeUnit('const openaiKey = "sk-proj-abc123-example";');
47+
const results = await detector.detect([unit], createContext());
48+
49+
// Should be detected as example API key (both example-api-key and example-openai-key may match)
50+
const hasExampleApiKey = results.some(r => r.metadata.patternId === 'example-api-key');
51+
expect(hasExampleApiKey).toBe(true);
52+
});
53+
54+
it('should detect GitHub PATs with example suffix', async () => {
55+
const unit = makeUnit('const githubToken = "ghp_1234567890abcdef1234567890abcdefexample";');
56+
const results = await detector.detect([unit], createContext());
57+
58+
// Should be detected as example GitHub PAT (placeholder-secret-value may also trigger, which is fine)
59+
const hasExampleGithubPat = results.some(r => r.metadata.patternId === 'example-github-pat');
60+
expect(hasExampleGithubPat).toBe(true);
61+
});
62+
63+
it('should detect placeholder secrets', async () => {
64+
const testCases = [
65+
'const secret = "password=example123";',
66+
'const apikey = "demo-key-12345";',
67+
'const token = "sample_token_abc123";',
68+
'const password = "changeme123";',
69+
];
70+
71+
for (const code of testCases) {
72+
const unit = makeUnit(code);
73+
const results = await detector.detect([unit], createContext());
74+
75+
// These should be detected as placeholder secrets
76+
const hasPlaceholderSecret = results.some(r =>
77+
r.metadata.patternId === 'placeholder-secret-value'
78+
);
79+
80+
if (!hasPlaceholderSecret) {
81+
console.log(`Code not detected: ${code}`);
82+
console.log('Results:', results);
83+
}
84+
85+
expect(hasPlaceholderSecret).toBe(true);
86+
}
87+
});
88+
89+
it('should detect AWS example keys', async () => {
90+
const unit = makeUnit('const awsKey = "AKIAIOSFODNN7EXAMPLE";');
91+
const results = await detector.detect([unit], createContext());
92+
93+
expect(results).toHaveLength(1);
94+
expect(results[0].message).toContain('AWS access key ID');
95+
expect(results[0].metadata.patternId).toBe('aws-access-key');
96+
});
97+
98+
it('should detect Stripe example keys', async () => {
99+
const unit = makeUnit('const stripeKey = "sk_test_abcdefghijklmnopqrstuvwxyzexample";');
100+
const results = await detector.detect([unit], createContext());
101+
102+
const hasStripeDetection = results.some(r =>
103+
r.metadata.patternId === 'example-stripe-key'
104+
);
105+
expect(hasStripeDetection).toBe(true);
106+
});
107+
108+
it('should NOT detect real-looking keys', async () => {
109+
const testCases = [
110+
{
111+
code: 'const token = "ghp_1234567890abcdef1234567890abcdef";',
112+
description: 'GitHub PAT without example suffix',
113+
// Note: example-openai-key is a general security check for ANY OpenAI key format,
114+
// so we only test non-OpenAI patterns here
115+
shouldMatchPatterns: ['github-pat-general'],
116+
},
117+
];
118+
119+
for (const testCase of testCases) {
120+
const unit = makeUnit(testCase.code);
121+
const results = await detector.detect([unit], createContext());
122+
123+
// These should NOT trigger example-specific patterns (those with "example" in the ID)
124+
const hasExampleSpecificDetection = results.some(r =>
125+
r.metadata.patternId === 'example-github-pat' ||
126+
r.metadata.patternId === 'example-api-key' ||
127+
r.metadata.patternId === 'placeholder-secret-basic'
128+
);
129+
130+
expect(hasExampleSpecificDetection).toBe(false);
131+
}
132+
});
133+
134+
it('should detect environment variable fallback secrets', async () => {
135+
const unit = makeUnit(`
136+
const apiKey = process.env.MY_API_KEY || "some-secret-value-here";
137+
`);
138+
const results = await detector.detect([unit], createContext());
139+
140+
const hasFallbackDetection = results.some(r =>
141+
r.metadata.patternId === 'env-var-fallback-secret-js'
142+
);
143+
144+
expect(hasFallbackDetection).toBe(true);
145+
});
146+
147+
it('should ignore environment variables that are properly used', async () => {
148+
const unit = makeUnit(`
149+
const apiKey = process.env.MY_API_KEY;
150+
if (!apiKey) throw new Error('API key required');
151+
`);
152+
const results = await detector.detect([unit], createContext());
153+
154+
// Should not detect as hardcoded secret when using process.env properly
155+
const hasHardcodedDetection = results.some(r =>
156+
r.metadata.patternId === 'hardcoded-api-key'
157+
);
158+
159+
expect(hasHardcodedDetection).toBe(false);
160+
});
161+
});

packages/core/tests/v4/security-pattern-detector.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ function handleRequest(req: Request) {
608608
// Test detection of ghp_ prefix + 36 alphanum chars (fake value)
609609
const unit = makeUnit('const token = "ghp_000000000000000000000000000000000000";');
610610
const results = await detector.detect([unit], createContext());
611-
const ghResult = results.find(r => r.metadata?.patternId === 'example-github-pat');
611+
const ghResult = results.find(r => r.metadata?.patternId === 'github-pat-general');
612612
expect(ghResult).toBeDefined();
613613
});
614614

0 commit comments

Comments
 (0)