Skip to content

Commit a715135

Browse files
authored
feat(secrets): detect Stripe, SendGrid, and Hugging Face keys (#2882)
Add three well-known, format-precise credential patterns across all four secret scanners in lockstep (PR-diff gate secrets-scan.ts, content-lane security-scan.ts, the shared safety.ts HARD_SECRET_KINDS, and the review-enrichment analyzer), matching the precision of the gitlab/npm rules so they are safe as unconditional hard blockers: - stripe_secret_key: sk_live_ / rk_live_ + >=24 base62 - sendgrid_key: SG. + 22-char id + . + 43-char secret - huggingface_token: hf_ + 34 base62 The SendGrid rule terminates with a negative lookahead (?![A-Za-z0-9_-]) rather than \b: because its final class includes -, a trailing \b would fail to match a key whose last character is - (a - before a quote/space is not a word boundary). Regression tests cover a hyphen-terminated key in every scanner. All test fixtures are assembled from fragments at runtime so the source never embeds a contiguous secret-shaped literal. Also updates the review-enrichment generic-assignment test, whose sk_live_ fixture now (correctly) trips the new Stripe rule, to use a non-format high-entropy value.
1 parent 5debc03 commit a715135

7 files changed

Lines changed: 111 additions & 2 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ const RULES: Rule[] = [
4242
re: /\bnpm_[A-Za-z0-9]{36}\b/,
4343
confidence: "high",
4444
},
45+
{
46+
// Stripe live secret / restricted key: `sk_live_` / `rk_live_` + >=24 base62.
47+
kind: "stripe_secret_key",
48+
re: /\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b/,
49+
confidence: "high",
50+
},
51+
{
52+
// SendGrid API key: `SG.` + 22-char id + `.` + 43-char secret (base64url).
53+
kind: "sendgrid_key",
54+
re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/,
55+
confidence: "high",
56+
},
57+
{
58+
// Hugging Face user access token: `hf_` + 34 base62 chars.
59+
kind: "huggingface_token",
60+
re: /\bhf_[A-Za-z0-9]{34}\b/,
61+
confidence: "high",
62+
},
4563
{
4664
kind: "private_key",
4765
re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ const hunk = (lines) => `@@ -1,0 +1,${lines.length} @@\n${lines.map((l) => `+${l
1414
const awsKeyFragmentA = "AKIA" + "IOSFODNN7"; // 13 chars — too short alone to match \bAKIA[0-9A-Z]{16}\b
1515
const awsKeyFragmentB = "EXAMPLE"; // matches no RULES pattern alone
1616
const fakeAwsKey = awsKeyFragmentA + awsKeyFragmentB;
17-
const fakeStripeKey = ["sk_live_", "abcdefghijklmnop1234567890"].join("");
17+
const fakeStripeKey = ["sk_live_", "abcdefghijklmnop1234567890"].join(""); // sk_live_ + 26 base62
18+
const fakeSendgridKey = ["SG.", "a".repeat(22), ".", "b".repeat(43)].join("");
19+
const fakeHuggingfaceToken = "hf_" + "a".repeat(34);
1820
const fakeGitlabToken = "glpat-" + "aBcDeFgHiJkLmNoPqRsT"; // 20 chars after the prefix
1921
const fakeNpmToken = "npm_" + "a".repeat(36);
22+
// A high-entropy value that matches NO format-specific rule, so it only trips the
23+
// generic keyword-assignment rule (built from fragments, never a contiguous literal).
24+
const fakeGenericValue = "aK9xQ2mZw7Ln" + "4Rv8Pt3Bh6Tc";
2025

2126
test("scanPatch flags a single-line AWS access key with high confidence", () => {
2227
const findings = scanPatch("src/config.ts", hunk([`const key = "${fakeAwsKey}";`]));
@@ -47,9 +52,40 @@ test("scanPatch flags an npm token with high confidence", () => {
4752
assert.equal(findings[0].confidence, "high");
4853
});
4954

50-
test("scanPatch flags a generic secret assignment", () => {
55+
test("scanPatch flags a Stripe live secret key with high confidence", () => {
5156
const findings = scanPatch("src/config.ts", hunk([`const apiKey = "${fakeStripeKey}";`]));
5257
assert.equal(findings.length, 1);
58+
assert.equal(findings[0].kind, "stripe_secret_key");
59+
assert.equal(findings[0].confidence, "high");
60+
});
61+
62+
test("scanPatch flags a SendGrid API key with high confidence", () => {
63+
const findings = scanPatch("src/config.ts", hunk([`const sg = "${fakeSendgridKey}";`]));
64+
assert.equal(findings.length, 1);
65+
assert.equal(findings[0].kind, "sendgrid_key");
66+
assert.equal(findings[0].confidence, "high");
67+
});
68+
69+
test("scanPatch flags a SendGrid key whose final secret character is a hyphen", () => {
70+
// Regression: a `\b` terminator would miss a key ending in `-`; the rule uses a
71+
// negative lookahead so the trailing hyphen still terminates the match.
72+
const hyphenTail = ["SG.", "a".repeat(22), ".", "b".repeat(42), "-"].join("");
73+
const findings = scanPatch("src/config.ts", hunk([`const sg = "${hyphenTail}";`]));
74+
assert.equal(findings.length, 1);
75+
assert.equal(findings[0].kind, "sendgrid_key");
76+
assert.equal(findings[0].confidence, "high");
77+
});
78+
79+
test("scanPatch flags a Hugging Face access token with high confidence", () => {
80+
const findings = scanPatch("src/config.ts", hunk([`const hfToken = "${fakeHuggingfaceToken}";`]));
81+
assert.equal(findings.length, 1);
82+
assert.equal(findings[0].kind, "huggingface_token");
83+
assert.equal(findings[0].confidence, "high");
84+
});
85+
86+
test("scanPatch flags a generic secret assignment", () => {
87+
const findings = scanPatch("src/config.ts", hunk([`const apiKey = "${fakeGenericValue}";`]));
88+
assert.equal(findings.length, 1);
5389
assert.equal(findings[0].kind, "generic_secret_assignment");
5490
assert.equal(findings[0].confidence, "medium");
5591
});

src/review/content-lane/security-scan.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const SECRET_PATTERNS: Array<{ name: string; re: RegExp }> = [
2222
{ name: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
2323
{ name: "gitlab_token", re: /\bglpat-[0-9A-Za-z_-]{20}\b/ },
2424
{ name: "npm_token", re: /\bnpm_[A-Za-z0-9]{36}\b/ },
25+
// Stripe live secret / restricted keys: `sk_live_` / `rk_live_` + >=24 base62.
26+
{ name: "stripe_secret_key", re: /\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b/ },
27+
// SendGrid API key: `SG.` + 22-char id + `.` + 43-char secret (base64url).
28+
{ name: "sendgrid_key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/ },
29+
// Hugging Face user access token: `hf_` + 34 base62 chars.
30+
{ name: "huggingface_token", re: /\bhf_[A-Za-z0-9]{34}\b/ },
2531
{ name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
2632
{ name: "seed_or_mnemonic", re: /\b(?:seed phrase|mnemonic)\b/i },
2733
{ name: "bittensor_key", re: /\b(?:hot|cold)key\b\s*[:=]/i },
@@ -119,6 +125,9 @@ const HARD_SECRET_KINDS = new Set([
119125
"google_api_key",
120126
"gitlab_token",
121127
"npm_token",
128+
"stripe_secret_key",
129+
"sendgrid_key",
130+
"huggingface_token",
122131
"jwt",
123132
"generic_secret_assignment",
124133
]);

src/review/safety.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const HARD_SECRET_KINDS = new Set([
3131
"google_api_key",
3232
"gitlab_token",
3333
"npm_token",
34+
"stripe_secret_key",
35+
"sendgrid_key",
36+
"huggingface_token",
3437
"jwt",
3538
"generic_secret_assignment",
3639
]);

src/review/secrets-scan.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const SECRET_PATTERNS: Array<{ name: string; re: RegExp }> = [
2222
{ name: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
2323
{ name: "gitlab_token", re: /\bglpat-[0-9A-Za-z_-]{20}\b/ },
2424
{ name: "npm_token", re: /\bnpm_[A-Za-z0-9]{36}\b/ },
25+
// Stripe live secret / restricted keys: `sk_live_` / `rk_live_` + >=24 base62.
26+
{ name: "stripe_secret_key", re: /\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b/ },
27+
// SendGrid API key: `SG.` + 22-char id + `.` + 43-char secret (base64url).
28+
{ name: "sendgrid_key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/ },
29+
// Hugging Face user access token: `hf_` + 34 base62 chars.
30+
{ name: "huggingface_token", re: /\bhf_[A-Za-z0-9]{34}\b/ },
2531
{ name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
2632
{ name: "seed_or_mnemonic", re: /\b(?:seed phrase|mnemonic)\b/i },
2733
{ name: "bittensor_key", re: /\b(?:hot|cold)key\b\s*[:=]/i },

test/unit/content-lane-security-scan.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ describe("scanForSecrets", () => {
3939
expect(scanForSecrets("npm_" + "a".repeat(36)).kinds).toContain("npm_token");
4040
});
4141

42+
it("flags Stripe, SendGrid, and Hugging Face keys (parity with the PR-diff gate)", () => {
43+
expect(scanForSecrets("sk_live_" + "a".repeat(24)).kinds).toContain("stripe_secret_key");
44+
expect(scanForSecrets("SG." + "a".repeat(22) + "." + "b".repeat(43)).kinds).toContain("sendgrid_key");
45+
expect(scanForSecrets("hf_" + "a".repeat(34)).kinds).toContain("huggingface_token");
46+
});
47+
48+
it("flags a SendGrid key whose final secret character is a hyphen", () => {
49+
// Regression: the terminator must not be `\b`, which would fail to match when the
50+
// final char of the `[A-Za-z0-9_-]` run is `-` (no word boundary before a quote/space).
51+
expect(scanForSecrets(`sg = "SG.${"a".repeat(22)}.${"b".repeat(42)}-"`).kinds).toContain("sendgrid_key");
52+
});
53+
4254
it("flags a generic secret/password/token assignment with a high-entropy value", () => {
4355
expect(scanForSecrets(`secret = "${GENERIC_VALUE}"`).kinds).toContain("generic_secret_assignment");
4456
expect(scanForSecrets(`api_key: '${GENERIC_VALUE}'`).kinds).toContain("generic_secret_assignment");
@@ -191,6 +203,9 @@ describe("secret-scan parity with the PR-diff gate (secrets-scan.ts)", () => {
191203
["aws_access_key", "AKIA" + "ABCDEFGHIJKLMNOP"],
192204
["private_key_block", "-----BEGIN OPENSSH " + "PRIVATE KEY-----"],
193205
["google_api_key", "AIza" + "SyABCDEFGHIJKLMNOPQRSTUVWXYZ0123456"],
206+
["stripe_secret_key", "sk_live_" + "a".repeat(24)],
207+
["sendgrid_key", "SG." + "a".repeat(22) + "." + "b".repeat(43)],
208+
["huggingface_token", "hf_" + "a".repeat(34)],
194209
["jwt", jwt],
195210
["generic_secret_assignment", `secret = "${GENERIC_VALUE}"`],
196211
["benign prose", "just normal documentation prose"],

test/unit/secrets-scan.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ describe("scanForSecrets — deterministic secret-pattern scanner", () => {
6565
expect(scanForSecrets(fakeToken).kinds).toContain("npm_token");
6666
});
6767

68+
it("flags a Stripe live secret key", () => {
69+
const fakeToken = "sk_live_" + "a".repeat(24);
70+
expect(scanForSecrets(fakeToken).kinds).toContain("stripe_secret_key");
71+
});
72+
73+
it("flags a SendGrid API key", () => {
74+
const fakeToken = "SG." + "a".repeat(22) + "." + "b".repeat(43);
75+
expect(scanForSecrets(fakeToken).kinds).toContain("sendgrid_key");
76+
});
77+
78+
it("flags a SendGrid API key whose final secret character is a hyphen", () => {
79+
// Regression: a `\b` terminator would miss a key ending in `-` (a `-` before a
80+
// quote/space is not a word boundary), so the rule uses a negative lookahead.
81+
const fakeToken = "SG." + "a".repeat(22) + "." + "b".repeat(42) + "-";
82+
expect(scanForSecrets(`sg = "${fakeToken}"`).kinds).toContain("sendgrid_key");
83+
});
84+
85+
it("flags a Hugging Face access token", () => {
86+
const fakeToken = "hf_" + "a".repeat(34);
87+
expect(scanForSecrets(fakeToken).kinds).toContain("huggingface_token");
88+
});
89+
6890
it("flags a JWT", () => {
6991
const fakeJwt = "eyJhbGciOiJIUzI1NiJ9" + "." + "eyJzdWIiOiIxMjM0NTY3ODkwIn0" + "." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
7092
expect(scanForSecrets(fakeJwt).kinds).toContain("jwt");

0 commit comments

Comments
 (0)