feat(redact): six new credential patterns — GitLab, HuggingFace, npm, DigitalOcean, Bearer, GCP SA (#1946)

Coverage gaps from the #1946 security review, including token types for
tooling gstack itself drives (glab):

HIGH (block): gitlab.token (glpat-/glptt-/gldt-), huggingface.token (hf_),
npm.token (npm_), digitalocean.token (dop_v1_), gcp.service_account (the
JSON-escaped "private_key" form that dodges pem.private_key's literal-block
match when minified, confirmed by "private_key_id" proximity).

MEDIUM (warn): auth.bearer — the most FP-prone shape in the set (docs are
full of "Authorization: Bearer <token>"), so it requires header-context
proximity and the same entropy>=3.0 + placeholder validator recipe as
env.kv. "Bearer YOUR_TOKEN_HERE" never fires; calibration over coverage,
per the cries-wolf principle.

All shapes are linear-time; test/redact-pattern-lint.test.ts covers them
automatically. Engine tests add positive + placeholder-negative cases per
pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-06-11 20:58:58 -07:00
parent 6bc00abb0f
commit 028cb52ca8
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 101 additions and 0 deletions

View File

@ -222,6 +222,48 @@ export const PATTERNS: RedactPattern[] = [
description: "GitHub fine-grained PAT",
regex: /\b(github_pat_[A-Za-z0-9_]{82})\b/,
},
{
id: "gitlab.token",
tier: "HIGH",
category: "secret",
description: "GitLab token (personal/pipeline-trigger/deploy)",
// glpat- personal access, glptt- pipeline trigger, gldt- deploy token.
// gstack drives glab first-class — these were a coverage gap (#1946).
regex: /\b(gl(?:pat|ptt|dt)-[A-Za-z0-9_-]{20,})\b/,
},
{
id: "huggingface.token",
tier: "HIGH",
category: "secret",
description: "HuggingFace access token",
regex: /\b(hf_[A-Za-z0-9]{30,})\b/,
},
{
id: "npm.token",
tier: "HIGH",
category: "secret",
description: "npm granular access token",
regex: /\b(npm_[A-Za-z0-9]{36})\b/,
},
{
id: "digitalocean.token",
tier: "HIGH",
category: "secret",
description: "DigitalOcean personal access token",
regex: /\b(dop_v1_[a-f0-9]{64})\b/,
},
{
id: "gcp.service_account",
tier: "HIGH",
category: "secret",
description: "GCP service-account JSON private key",
// The JSON-escaped form ("private_key": "-----BEGIN PRIVATE KEY-----\n...)
// dodges pem.private_key's literal-block match when minified to one line.
// Proximity to "private_key_id" confirms the GCP service-account shape.
regex: /("private_key"\s*:\s*"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----)/,
nearRegex: /"private_key_id"/,
nearWindow: 300,
},
{
id: "anthropic.key",
tier: "HIGH",
@ -352,6 +394,22 @@ export const PATTERNS: RedactPattern[] = [
!/^\$\{?[A-Za-z_]/.test(span) &&
shannonEntropy(span) >= 3.0,
},
{
id: "auth.bearer",
tier: "MEDIUM",
category: "secret",
description: "Authorization Bearer token (high-entropy, header context)",
// FP-prone shape (docs and examples are full of "Bearer <token>"), so:
// MEDIUM tier, requires "authorization" nearby, and the same entropy
// recipe as env.kv to kill Bearer YOUR_TOKEN_HERE placeholders.
regex: /\bBearer[ \t]+([A-Za-z0-9._~+/=-]{20,})\b/,
nearRegex: /authorization/i,
nearWindow: 80,
validate: (span) =>
!isPlaceholderSpan(span) &&
!/^\$\{?[A-Za-z_]/.test(span) &&
shannonEntropy(span) >= 3.0,
},
// ===== MEDIUM — PII (auto-redactable subset) =====
{

View File

@ -42,6 +42,17 @@ describe("HIGH credential patterns", () => {
["slack.webhook", "https://hooks.slack.com/services/T00000000/B11111111/" + "a".repeat(24)],
["discord.webhook", "https://discord.com/api/webhooks/123456789012345678/" + "a".repeat(60)],
["pem.private_key", "-----BEGIN RSA PRIVATE KEY-----"],
// #1946 coverage-gap additions
["gitlab.token", "remote: glpat-" + "Ab12Cd34Ef56Gh78Ij90"],
["gitlab.token", "trigger glptt-" + "a1b2c3d4e5f6a7b8c9d0e1f2"],
["gitlab.token", "deploy gldt-" + "Zy98Xw76Vu54Ts32Rq10"],
["huggingface.token", "hf_" + "AbCdEfGhIjKlMnOpQrStUvWxYz012345"],
["npm.token", "npm_" + "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"],
["digitalocean.token", "dop_v1_" + "0123456789abcdef".repeat(4)],
[
"gcp.service_account",
'{"private_key_id": "abc123", "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIE..."}',
],
];
for (const [id, text] of cases) {
test(`flags ${id}`, () => {
@ -121,6 +132,38 @@ describe("MEDIUM demoted credential-shaped patterns (TENSION-1)", () => {
expect(ids("API_KEY=changeme")).not.toContain("env.kv");
expect(ids("API_KEY=${MY_VAR}")).not.toContain("env.kv");
});
// #1946 — Bearer is the most FP-prone shape in the wave: docs and examples
// are full of "Authorization: Bearer <token>". MEDIUM + header proximity +
// the env.kv entropy recipe keep it calibrated.
test("auth.bearer fires on a high-entropy token in header context", () => {
const text = "curl -H 'Authorization: Bearer 8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJq'";
const f = scan(text, { repoVisibility: "private" }).findings.find(
(x) => x.id === "auth.bearer",
);
expect(f).toBeDefined();
expect(f?.tier).toBe("MEDIUM");
});
test("auth.bearer skips placeholders and env interpolations", () => {
expect(ids("Authorization: Bearer YOUR_TOKEN_HERE_PLACEHOLDER")).not.toContain("auth.bearer");
expect(ids("Authorization: Bearer ${ACCESS_TOKEN_FROM_ENV}")).not.toContain("auth.bearer");
});
test("auth.bearer requires header context (bare 'Bearer x' prose doesn't fire)", () => {
expect(ids("the Bearer 8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJq walked in")).not.toContain(
"auth.bearer",
);
});
});
describe("#1946 pattern negatives (placeholders never fire)", () => {
test("short or placeholder shapes don't trip the new HIGH patterns", () => {
expect(ids("glpat-xxxx")).not.toContain("gitlab.token");
expect(ids("hf_token")).not.toContain("huggingface.token");
expect(ids("npm_install")).not.toContain("npm.token");
expect(ids("dop_v1_short")).not.toContain("digitalocean.token");
// pem header WITHOUT the GCP JSON shape stays pem.private_key only.
expect(ids("-----BEGIN PRIVATE KEY-----")).not.toContain("gcp.service_account");
});
});
describe("PII patterns", () => {