179 lines
7.0 KiB
JavaScript
179 lines
7.0 KiB
JavaScript
import { describe, it, expect } from "vitest";
|
|
import path from "node:path";
|
|
import {
|
|
scanText,
|
|
isAllowlistedPath,
|
|
lineHasPlaceholder,
|
|
PATTERNS,
|
|
} from "./verify-secrets.mjs";
|
|
|
|
describe("verify-secrets pattern detection", () => {
|
|
it("detects a GitHub PAT in plain text", () => {
|
|
const sample = `const token = "ghp_AbCdEfGhIjKlMnOpQrStUv0123456789";`;
|
|
const findings = scanText(sample, { filename: "apps/web/secret.ts" });
|
|
expect(findings.length).toBeGreaterThanOrEqual(1);
|
|
expect(findings.some((f) => f.patternId === "github-pat")).toBe(true);
|
|
});
|
|
|
|
it("detects a GitHub fine-grained PAT", () => {
|
|
const sample = `const t = "github_pat_11ABCDEFG0123456789abcdef";`;
|
|
const findings = scanText(sample, { filename: "apps/api/foo.ts" });
|
|
expect(findings.some((f) => f.patternId === "github-fine-grained-pat")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("detects a GitLab PAT", () => {
|
|
const sample = `const t = "glpat-abcDEF1234567890_xyzQR";`;
|
|
const findings = scanText(sample, { filename: "apps/web/x.ts" });
|
|
expect(findings.some((f) => f.patternId === "gitlab-pat")).toBe(true);
|
|
});
|
|
|
|
it("detects an AWS Access Key ID", () => {
|
|
const sample = `AKIAIOSFODNN7EXAMPLE2`;
|
|
const findings = scanText(sample, { filename: "apps/web/aws.ts" });
|
|
// Note: this sample contains "example" placeholder so it should be skipped.
|
|
expect(findings.length).toBe(0);
|
|
});
|
|
|
|
it("detects a real AWS Access Key ID on a non-placeholder line", () => {
|
|
const sample = `const k = "AKIAZZZZZZZZZZZZZZZZ";`;
|
|
const findings = scanText(sample, { filename: "apps/web/aws.ts" });
|
|
expect(findings.some((f) => f.patternId === "aws-access-key-id")).toBe(true);
|
|
});
|
|
|
|
it("detects Stripe live secret keys but not test keys", () => {
|
|
const live = `const k = "sk_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaa";`;
|
|
const test = `const k = "sk_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaa";`;
|
|
const liveFindings = scanText(live, { filename: "apps/web/s.ts" });
|
|
const testFindings = scanText(test, { filename: "apps/web/s.ts" });
|
|
expect(liveFindings.some((f) => f.patternId === "stripe-secret-live")).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
testFindings.some((f) => f.patternId === "stripe-secret-live"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("detects Stripe webhook secrets only when the line contains 'live'", () => {
|
|
const withLive = `const k = "whsec_aaaaaaaaaaaaaaaaaaaaaaaaaaaa"; // live prod webhook`;
|
|
const withoutLive = `const k = "whsec_aaaaaaaaaaaaaaaaaaaaaaaaaaaa";`;
|
|
const a = scanText(withLive, { filename: "apps/web/w.ts" });
|
|
const b = scanText(withoutLive, { filename: "apps/web/w.ts" });
|
|
expect(a.some((f) => f.patternId === "stripe-webhook-live")).toBe(true);
|
|
expect(b.some((f) => f.patternId === "stripe-webhook-live")).toBe(false);
|
|
});
|
|
|
|
it("detects Slack bot tokens", () => {
|
|
const sample = `const t = "xoxb-1234567890-1234567890-1234567890-abcdef1234567890";`;
|
|
const findings = scanText(sample, { filename: "apps/web/slack.ts" });
|
|
expect(findings.some((f) => f.patternId === "slack-bot-token")).toBe(true);
|
|
});
|
|
|
|
it("detects a PEM private key header", () => {
|
|
const sample = `-----BEGIN RSA PRIVATE KEY-----`;
|
|
const findings = scanText(sample, { filename: "apps/web/key.pem" });
|
|
expect(findings.some((f) => f.patternId === "private-key-header")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("detects a JWT-shaped string", () => {
|
|
const sample = `const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";`;
|
|
const findings = scanText(sample, { filename: "apps/web/jwt.ts" });
|
|
expect(findings.some((f) => f.patternId === "jwt-like")).toBe(true);
|
|
});
|
|
|
|
it("detects generic high-entropy assignments to secret-named identifiers", () => {
|
|
const sample = `const password = "abcdef0123456789abcdef0123456789ZZ";`;
|
|
const findings = scanText(sample, { filename: "apps/web/pw.ts" });
|
|
expect(
|
|
findings.some((f) => f.patternId === "generic-high-entropy-assignment"),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("verify-secrets placeholder false-positive suppression", () => {
|
|
it("ignores lines containing 'placeholder'", () => {
|
|
const sample = `const token = "ghp_ZZZZZZZZZZZZZZZZZZZZ"; // placeholder only`;
|
|
expect(scanText(sample, { filename: "x.ts" })).toHaveLength(0);
|
|
});
|
|
|
|
it("ignores lines containing 'example'", () => {
|
|
const sample = `# example: AKIAIOSFODNN7EXAMPL1`;
|
|
expect(scanText(sample, { filename: "x.ts" })).toHaveLength(0);
|
|
});
|
|
|
|
it("ignores 'your-' style placeholders", () => {
|
|
const sample = `STRIPE_KEY=sk_live_your-real-stripe-secret-here-1234`;
|
|
expect(scanText(sample, { filename: "x.ts" })).toHaveLength(0);
|
|
});
|
|
|
|
it("ignores angle-bracket placeholders", () => {
|
|
const sample = `TOKEN=ghp_<paste-token-here-with-padding>`;
|
|
expect(scanText(sample, { filename: "x.ts" })).toHaveLength(0);
|
|
});
|
|
|
|
it("ignores shell/JS interpolation", () => {
|
|
const sample = "const k = `ghp_${process.env.GITHUB_TOKEN}aaaaaaaaaaaaaaaaaaaaa`;";
|
|
expect(scanText(sample, { filename: "x.ts" })).toHaveLength(0);
|
|
});
|
|
|
|
it("ignores 'REDACTED' / 'xxxxx' / 'dummy' tokens", () => {
|
|
expect(lineHasPlaceholder("ghp_REDACTED_VALUE")).toBe(true);
|
|
expect(lineHasPlaceholder("AKIAxxxxxYYYYYY")).toBe(true);
|
|
expect(lineHasPlaceholder("token=dummy_value_here")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("verify-secrets allowlist semantics", () => {
|
|
it("treats .env.example as allowlisted", () => {
|
|
const p = path.join(process.cwd(), "apps", "web", ".env.example");
|
|
expect(isAllowlistedPath(p)).toBe(true);
|
|
});
|
|
|
|
it("treats .env.production.example as allowlisted", () => {
|
|
expect(
|
|
isAllowlistedPath("/repo/.env.production.example"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats .env.fly.example and .env.test as allowlisted", () => {
|
|
expect(isAllowlistedPath("/repo/.env.fly.example")).toBe(true);
|
|
expect(isAllowlistedPath("/repo/.env.test")).toBe(true);
|
|
});
|
|
|
|
it("treats test/spec/fixture/mock/stub paths as allowlisted", () => {
|
|
expect(isAllowlistedPath("/repo/apps/web/x.test.ts")).toBe(true);
|
|
expect(isAllowlistedPath("/repo/apps/web/x.spec.ts")).toBe(true);
|
|
expect(isAllowlistedPath("/repo/scripts/fixtures/foo.json")).toBe(true);
|
|
expect(isAllowlistedPath("/repo/apps/web/mocks/server.ts")).toBe(true);
|
|
expect(isAllowlistedPath("/repo/apps/web/stubs/foo.ts")).toBe(true);
|
|
});
|
|
|
|
it("does not allowlist arbitrary source files", () => {
|
|
expect(isAllowlistedPath("/repo/apps/web/routes/login.tsx")).toBe(false);
|
|
expect(isAllowlistedPath("/repo/packages/db/src/client.ts")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("verify-secrets registry shape", () => {
|
|
it("exposes all expected pattern ids", () => {
|
|
const ids = PATTERNS.map((p) => p.id);
|
|
expect(ids).toEqual(
|
|
expect.arrayContaining([
|
|
"github-pat",
|
|
"github-fine-grained-pat",
|
|
"gitlab-pat",
|
|
"aws-access-key-id",
|
|
"stripe-secret-live",
|
|
"stripe-webhook-live",
|
|
"slack-bot-token",
|
|
"private-key-header",
|
|
"jwt-like",
|
|
"generic-high-entropy-assignment",
|
|
]),
|
|
);
|
|
});
|
|
});
|