From 028cb52ca8aa2b39b6e755abcc5be78b14c61165 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 20:58:58 -0700 Subject: [PATCH] =?UTF-8?q?feat(redact):=20six=20new=20credential=20patter?= =?UTF-8?q?ns=20=E2=80=94=20GitLab,=20HuggingFace,=20npm,=20DigitalOcean,?= =?UTF-8?q?=20Bearer,=20GCP=20SA=20(#1946)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 "), 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 --- lib/redact-patterns.ts | 58 ++++++++++++++++++++++++++++++++++++++ test/redact-engine.test.ts | 43 ++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 0645bfe1c..76b81f3d2 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -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 "), 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) ===== { diff --git a/test/redact-engine.test.ts b/test/redact-engine.test.ts index 1300e94cb..75908a983 100644 --- a/test/redact-engine.test.ts +++ b/test/redact-engine.test.ts @@ -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 ". 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", () => {