mirror of https://github.com/garrytan/gstack.git
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:
parent
6bc00abb0f
commit
028cb52ca8
|
|
@ -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) =====
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue