Skip to content

Commit 295c213

Browse files
feat(enrichment): detect Discord bot tokens and Twilio SIDs in secret-scan (#3263)
* feat(enrichment): detect Discord bot tokens and Twilio SIDs in secret-scan Add high-confidence patterns for Discord bot tokens and Twilio Account/API Key SIDs with fragment-based fixtures, truncation negatives, and webhook parity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(enrichment): tighten Twilio SID tail boundary in secret-scan Use identifier-continuation lookahead so AC/SK + 32 hex is not matched when immediately followed by a non-hex identifier char (Orb #3262). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ae0faa8 commit 295c213

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

review-enrichment/src/analyzers/secret-scan.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,24 @@ const RULES: Rule[] = [
169169
re: /\bkey-[0-9A-Za-z]{32}\b/,
170170
confidence: "high",
171171
},
172+
{
173+
// Discord bot token: `[MNO]` + 23 base64url chars, `.`, 6-char segment, `.`, 27-char segment.
174+
kind: "discord_bot_token",
175+
re: /\b[MNO][A-Za-z0-9_-]{23}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}(?![A-Za-z0-9_-])/,
176+
confidence: "high",
177+
},
178+
{
179+
// Twilio Account SID: `AC` + 32 hex chars (distinct from Auth Token, which has no prefix).
180+
kind: "twilio_account_sid",
181+
re: /\bAC[0-9a-fA-F]{32}(?![A-Za-z0-9_])/,
182+
confidence: "high",
183+
},
184+
{
185+
// Twilio API Key SID: `SK` + 32 hex chars.
186+
kind: "twilio_api_key_sid",
187+
re: /\bSK[0-9a-fA-F]{32}(?![A-Za-z0-9_])/,
188+
confidence: "high",
189+
},
172190
{
173191
// Google OAuth 2.0 client secret: `GOCSPX-` + 28 base64url chars.
174192
kind: "google_oauth_client_secret",

review-enrichment/test/secret-scan.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,62 @@ test("scanPatch does not flag a Mailgun-shaped key with an invalid body characte
415415
assert.equal(findings.length, 0);
416416
});
417417

418+
test("scanPatch flags a Discord bot token with high confidence", () => {
419+
const fakeDiscordBotToken = ["M", "A".repeat(23), ".", "b".repeat(6), ".", "c".repeat(27)].join("");
420+
const findings = scanPatch("src/config.ts", hunk([`const discord = "${fakeDiscordBotToken}";`]));
421+
assert.equal(findings.length, 1);
422+
assert.equal(findings[0].kind, "discord_bot_token");
423+
assert.equal(findings[0].confidence, "high");
424+
});
425+
426+
test("scanPatch does not flag a truncated Discord bot token", () => {
427+
const truncated = ["M", "A".repeat(23), ".", "b".repeat(6), ".", "c".repeat(26)].join("");
428+
const findings = scanPatch("src/config.ts", hunk([`const discord = "${truncated}";`]));
429+
assert.equal(findings.length, 0);
430+
});
431+
432+
test("scanPatch does not classify a Discord bot token as a webhook URL", () => {
433+
const fakeDiscordBotToken = ["N", "B".repeat(23), ".", "d".repeat(6), ".", "e".repeat(27)].join("");
434+
const findings = scanPatch("src/config.ts", hunk([`const discord = "${fakeDiscordBotToken}";`]));
435+
assert.equal(findings.length, 1);
436+
assert.equal(findings[0].kind, "discord_bot_token");
437+
assert.equal(findings.some((f) => f.kind === "discord_webhook_url"), false);
438+
});
439+
440+
test("scanPatch flags Twilio Account and API Key SIDs with high confidence", () => {
441+
const fakeTwilioAccountSid = "AC" + "a".repeat(32);
442+
const fakeTwilioApiKeySid = "SK" + "b".repeat(32);
443+
const accountFindings = scanPatch("src/config.ts", hunk([`const sid = "${fakeTwilioAccountSid}";`]));
444+
assert.equal(accountFindings.length, 1);
445+
assert.equal(accountFindings[0].kind, "twilio_account_sid");
446+
assert.equal(accountFindings[0].confidence, "high");
447+
448+
const keyFindings = scanPatch("src/config.ts", hunk([`const apiKey = "${fakeTwilioApiKeySid}";`]));
449+
assert.equal(keyFindings.length, 1);
450+
assert.equal(keyFindings[0].kind, "twilio_api_key_sid");
451+
assert.equal(keyFindings[0].confidence, "high");
452+
});
453+
454+
test("scanPatch does not flag truncated Twilio SIDs or identifier continuation past 32 hex chars", () => {
455+
const truncated = "AC" + "a".repeat(31);
456+
assert.equal(scanPatch("src/config.ts", hunk([`const sid = "${truncated}";`])).length, 0);
457+
const hexOverrun = "AC" + "a".repeat(32) + "f";
458+
assert.equal(
459+
scanPatch("src/config.ts", hunk([`const sid = "${hexOverrun}";`])).some((f) => f.kind === "twilio_account_sid"),
460+
false,
461+
);
462+
const nonHexTail = "AC" + "a".repeat(32) + "z";
463+
assert.equal(
464+
scanPatch("src/config.ts", hunk([`const sid = "${nonHexTail}";`])).some((f) => f.kind === "twilio_account_sid"),
465+
false,
466+
);
467+
const skNonHexTail = "SK" + "b".repeat(32) + "z";
468+
assert.equal(
469+
scanPatch("src/config.ts", hunk([`const key = "${skNonHexTail}";`])).some((f) => f.kind === "twilio_api_key_sid"),
470+
false,
471+
);
472+
});
473+
418474
test("scanPatch flags additional high-confidence SaaS/cloud/CI credential formats", () => {
419475
const cases = [
420476
["google_oauth_client_secret", "GOCSPX-" + b62(28)],

0 commit comments

Comments
 (0)