import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import {
registerPluginSecretPatterns,
resetPluginSecretPatternsForTests,
unregisterPluginSecretPatterns,
} from "../security/plugin-secret-patterns.js";
import {
_isPlaceholder,
redactSecrets,
redactSecretsWith,
scanText,
type SecretMatch,
} from "../security/secret-scanner.js";
// ---------------------------------------------------------------------------
// Helper: assert a single match of the expected type
// ---------------------------------------------------------------------------
function expectMatch(text: string, expectedType: string): SecretMatch {
const matches = scanText(text);
const found = matches.find((m) => m.type === expectedType);
expect(found).toBeDefined();
return found!;
}
function expectNoMatch(text: string): void {
const matches = scanText(text);
expect(matches).toHaveLength(0);
}
// ---------------------------------------------------------------------------
// AWS
// ---------------------------------------------------------------------------
describe("AWS keys", () => {
test("detects AWS access key ID", () => {
expectMatch("aws_access_key_id = AKIAIOSFODNN7REALKEY", "AWS Access Key");
});
test("does not flag the AWS example key", () => {
const matches = scanText("AKIAIOSFODNN7EXAMPLE");
const aws = matches.filter((m) => m.type === "AWS Access Key");
expect(aws).toHaveLength(0);
});
test("detects AWS secret key after separator", () => {
// Exactly 40 base-64 chars with mixed case and / (distinguishes from hex)
const secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYRE+LK3Yzab";
expect(secret.length).toBe(40);
expectMatch(`aws_secret_access_key = "${secret}"`, "AWS Secret Key");
});
test("does not flag AWS example secret key", () => {
const matches = scanText(
'secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"',
);
const aws = matches.filter((m) => m.type === "AWS Secret Key");
expect(aws).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// GitHub
// ---------------------------------------------------------------------------
describe("GitHub tokens", () => {
test("detects ghp_ personal access token", () => {
expectMatch(`token=ghp_${"A".repeat(36)}`, "GitHub Token");
});
test("detects gho_ OAuth token", () => {
expectMatch(`gho_${"B".repeat(36)}`, "GitHub Token");
});
test("detects fine-grained PAT", () => {
expectMatch(`github_pat_${"C".repeat(30)}`, "GitHub Fine-Grained PAT");
});
});
// ---------------------------------------------------------------------------
// GitLab
// ---------------------------------------------------------------------------
describe("GitLab tokens", () => {
test("detects glpat- token", () => {
expectMatch("glpat-abcDEF1234567890abcde", "GitLab Token");
});
});
// ---------------------------------------------------------------------------
// Stripe
// ---------------------------------------------------------------------------
describe("Stripe keys", () => {
test("detects live secret key", () => {
expectMatch(`sk_live_${"a".repeat(24)}`, "Stripe Secret Key");
});
test("detects live restricted key", () => {
expectMatch(`rk_live_${"b".repeat(24)}`, "Stripe Restricted Key");
});
test("does not flag test keys", () => {
const matches = scanText(`sk_test_${"c".repeat(24)}`);
const stripe = matches.filter((m) => m.type === "Stripe Secret Key");
expect(stripe).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Slack
// ---------------------------------------------------------------------------
describe("Slack tokens", () => {
test("detects bot token", () => {
expectMatch(
"xoxb-1234567890-1234567890-aBcDeFgHiJkLmNoPqRsTuVwX",
"Slack Bot Token",
);
});
test("detects webhook URL", () => {
expectMatch(
"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
"Slack Webhook",
);
});
});
// ---------------------------------------------------------------------------
// Telegram
// ---------------------------------------------------------------------------
describe("Telegram bot tokens", () => {
// Build test token at runtime to avoid tripping pre-commit secret hook
const BOT_TOKEN = [
"123456789",
":",
"ABCDefGHIJklmnopQRSTuvwxyz012345678",
].join("");
test("detects Telegram bot token", () => {
expectMatch(`token=${BOT_TOKEN}`, "Telegram Bot Token");
});
test("detects bot token in surrounding text", () => {
expectMatch(
`My bot token is ${BOT_TOKEN} please save it`,
"Telegram Bot Token",
);
});
test("detects bot token ending with hyphen", () => {
// ~1.5% of valid tokens end with '-'; trailing \b would miss these
const tokenEndingHyphen = [
"123456789",
":",
"ABCDefGHIJklmnopQRSTuvwxyz01234567-",
].join("");
expectMatch(`token=${tokenEndingHyphen}`, "Telegram Bot Token");
});
test("does not flag short numeric:alpha strings", () => {
// Too few digits in bot ID (only 5)
const matches = scanText("12345:ABCDefGHIJklmnopQRSTuvwxyz012345678");
const telegram = matches.filter((m) => m.type === "Telegram Bot Token");
expect(telegram).toHaveLength(0);
});
test("does not flag token with wrong secret length", () => {
// Secret part is only 10 chars (needs 35)
const matches = scanText("123456789:ABCDefGHIJ");
const telegram = matches.filter((m) => m.type === "Telegram Bot Token");
expect(telegram).toHaveLength(0);
});
test("does not flag token with too-long secret part", () => {
// Secret part is 40 chars (needs exactly 35)
const matches = scanText(
"123456789:ABCDefGHIJklmnopQRSTuvwxyz0123456789AB",
);
const telegram = matches.filter((m) => m.type === "Telegram Bot Token");
expect(telegram).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Anthropic
// ---------------------------------------------------------------------------
describe("Anthropic keys", () => {
test("detects sk-ant- key", () => {
expectMatch(`sk-ant-${"a".repeat(80)}`, "Anthropic API Key");
});
});
// ---------------------------------------------------------------------------
// OpenAI
// ---------------------------------------------------------------------------
describe("OpenAI keys", () => {
test("detects sk-proj- key", () => {
expectMatch(`sk-proj-${"a".repeat(40)}`, "OpenAI Project Key");
});
test("detects classic OpenAI key format", () => {
expectMatch(
`sk-${"a".repeat(20)}T3BlbkFJ${"b".repeat(20)}`,
"OpenAI API Key",
);
});
});
// ---------------------------------------------------------------------------
// Google
// ---------------------------------------------------------------------------
describe("Google keys", () => {
test("detects AIza key", () => {
// AIza + exactly 35 alphanumeric/dash/underscore chars
const key = "AIza" + "SyA1bcDefGHijklMnoPQRStuvWXyz012345";
expect(key.slice(4).length).toBe(35);
expectMatch(key, "Google API Key");
});
test("detects GOCSPX- client secret", () => {
// GOCSPX- + exactly 28 chars
const key = "GOCSPX-" + "aBcDeFgHiJkLmNoPqRsTuVwXy123";
expect(key.slice(7).length).toBe(28);
expectMatch(key, "Google OAuth Client Secret");
});
});
// ---------------------------------------------------------------------------
// Twilio
// ---------------------------------------------------------------------------
describe("Twilio keys", () => {
test("detects SK key", () => {
expectMatch(`SK${"a".repeat(32)}`, "Twilio API Key");
});
});
// ---------------------------------------------------------------------------
// SendGrid
// ---------------------------------------------------------------------------
describe("SendGrid keys", () => {
test("detects SG. key", () => {
expectMatch(`SG.${"a".repeat(22)}.${"b".repeat(43)}`, "SendGrid API Key");
});
});
// ---------------------------------------------------------------------------
// Mailgun
// ---------------------------------------------------------------------------
describe("Mailgun keys", () => {
test("detects key- format", () => {
expectMatch(`key-${"c".repeat(32)}`, "Mailgun API Key");
});
});
// ---------------------------------------------------------------------------
// npm
// ---------------------------------------------------------------------------
describe("npm tokens", () => {
test("detects npm_ token", () => {
expectMatch(`npm_${"d".repeat(36)}`, "npm Token");
});
});
// ---------------------------------------------------------------------------
// PyPI
// ---------------------------------------------------------------------------
describe("PyPI tokens", () => {
test("detects pypi- token", () => {
expectMatch(`pypi-${"e".repeat(50)}`, "PyPI API Token");
});
});
// ---------------------------------------------------------------------------
// Private keys
// ---------------------------------------------------------------------------
describe("private keys", () => {
test("detects RSA private key header", () => {
expectMatch("-----BEGIN RSA PRIVATE KEY-----\nMIIE...", "Private Key");
});
test("detects generic private key header", () => {
expectMatch("-----BEGIN PRIVATE KEY-----\nMIIE...", "Private Key");
});
test("detects EC private key header", () => {
expectMatch("-----BEGIN EC PRIVATE KEY-----\nMIIE...", "Private Key");
});
test("detects OPENSSH private key header", () => {
expectMatch(
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNz...",
"Private Key",
);
});
});
// ---------------------------------------------------------------------------
// JWT
// ---------------------------------------------------------------------------
describe("JSON Web Tokens", () => {
test("detects JWT", () => {
// A structurally valid JWT (base64url-encoded header.payload.signature)
const header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
const payload =
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
const signature = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
expectMatch(`${header}.${payload}.${signature}`, "JSON Web Token");
});
});
// ---------------------------------------------------------------------------
// Connection strings
// ---------------------------------------------------------------------------
describe("connection strings", () => {
test("detects postgres connection string", () => {
expectMatch(
"postgres://user:secret@db.example.com:5432/mydb",
"Database Connection String",
);
});
test("detects mongodb+srv connection string", () => {
expectMatch(
"mongodb+srv://user:password@cluster.mongodb.net/db",
"Database Connection String",
);
});
test("detects redis connection string", () => {
expectMatch(
"redis://default:password@redis.example.com:6379",
"Database Connection String",
);
});
test("detects mysql connection string", () => {
expectMatch(
"mysql://root:secret@localhost:3306/app",
"Database Connection String",
);
});
});
// ---------------------------------------------------------------------------
// Generic secret assignment
// ---------------------------------------------------------------------------
describe("generic secret assignments", () => {
test('detects password = "value"', () => {
expectMatch('password = "SuperSecret123!"', "Generic Secret Assignment");
});
test('detects api_key: "value"', () => {
expectMatch(
"api_key: 'my-real-api-key-value'",
"Generic Secret Assignment",
);
});
test("detects SECRET=value in quotes", () => {
expectMatch(
'SECRET="a-very-long-secret-value"',
"Generic Secret Assignment",
);
});
test("ignores short values (< 8 chars)", () => {
// "short" is only 5 chars, should not match generic pattern
const matches = scanText('password = "short"');
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Placeholder / false-positive suppression
// ---------------------------------------------------------------------------
describe("placeholder detection", () => {
test("suppresses known placeholder values", () => {
expectNoMatch('password = "changeme"');
expectNoMatch('password = "password"');
expectNoMatch('password = "xxxxxxxx"');
});
test("suppresses test-prefixed keys", () => {
expectNoMatch(`sk_test_${"a".repeat(24)}`);
expectNoMatch(`pk_test_${"b".repeat(24)}`);
});
test("suppresses values containing example/placeholder/dummy", () => {
expectNoMatch('token = "my-example-api-key-value"');
expectNoMatch('key = "this-is-a-placeholder-string"');
expectNoMatch('secret = "dummy-value-for-testing"');
});
test("suppresses all-same-character strings", () => {
expectNoMatch('password = "aaaaaaaa"');
});
test("isPlaceholder returns true for known values", () => {
expect(_isPlaceholder("AKIAIOSFODNN7EXAMPLE")).toBe(true);
expect(_isPlaceholder("your-api-key-here")).toBe(true);
expect(_isPlaceholder("changeme")).toBe(true);
});
test("isPlaceholder returns false for real-looking values", () => {
expect(_isPlaceholder("wJalrXUtnFEMI/K7MDENG/bPxRfiCYREALKEY")).toBe(false);
expect(_isPlaceholder("sk_live_abcdefghij1234567890abcd")).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Redaction
// ---------------------------------------------------------------------------
describe("redaction", () => {
test("redactSecrets replaces secrets in text", () => {
const input = `export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7REALKEY`;
const result = redactSecrets(input);
expect(result).toContain('');
expect(result).not.toContain("AKIAIOSFODNN7REALKEY");
});
test("redactSecrets preserves text without secrets", () => {
const input = "Hello world, this is safe text.";
expect(redactSecrets(input)).toBe(input);
});
test("redactSecrets handles multiple secrets", () => {
const input = `
AWS_KEY=AKIAIOSFODNN7REALKEY
TOKEN=ghp_${"A".repeat(36)}
`;
const result = redactSecrets(input);
expect(result).toContain('');
expect(result).toContain('');
});
});
// ---------------------------------------------------------------------------
// PEM private-key body redaction — the scanner replaces the matched span in
// place, so the match must cover header→body→footer or the base64 body and
// END footer survive redaction (the header-only-matcher leak this fixes).
// Synthetic fake PEM material only — never a real key.
// ---------------------------------------------------------------------------
describe("private-key block redaction", () => {
const FAKE_PEM_BODY =
"MIIFAKEfakefakefakefakefakefakefakefakefake\n" +
"FAKEfakefakefakefakefakefakefakefakefake==";
const FULL_PEM_BLOCK = `-----BEGIN RSA PRIVATE KEY-----\n${FAKE_PEM_BODY}\n-----END RSA PRIVATE KEY-----`;
test("redactSecrets removes the whole block — body and footer, not just the header", () => {
const input = `here is the key:\n${FULL_PEM_BLOCK}\nuse it well`;
const result = redactSecrets(input);
expect(result).toContain('');
// The base64 body must not survive.
expect(result).not.toContain("MIIFAKE");
expect(result).not.toContain("FAKEfake");
// Header and footer must not survive either.
expect(result).not.toContain("-----BEGIN");
expect(result).not.toContain("-----END");
// Surrounding text is preserved.
expect(result).toContain("here is the key:");
expect(result).toContain("use it well");
});
test("redactSecretsWith removes the whole block", () => {
const result = redactSecretsWith(FULL_PEM_BLOCK, () => "[KEY]");
expect(result).toBe("[KEY]");
expect(result).not.toContain("MIIFAKE");
expect(result).not.toContain("-----END");
});
});
// ---------------------------------------------------------------------------
// Plugin-declared patterns (runtime registry)
// ---------------------------------------------------------------------------
describe("plugin-declared patterns", () => {
// Synthetic token mirroring the incident's shape (never a real credential).
const pluginKey = "virlo_tkn_Qm7pW2xLbV9sKjR4tNcY8dZh3Fg";
const registerVirlo = () =>
registerPluginSecretPatterns("virlo", [
{ label: "Virlo API Key", pattern: "virlo_tkn_[A-Za-z0-9_-]{20,}" },
]);
beforeEach(() => {
resetPluginSecretPatternsForTests();
});
afterEach(() => {
resetPluginSecretPatternsForTests();
});
test("scanText detects a registered plugin key inside larger text", () => {
registerVirlo();
const input = `The setup doc pastes ${pluginKey} inline`;
const match = expectMatch(input, "Virlo API Key (plugin:virlo)");
expect(input.slice(match.startIndex, match.endIndex)).toBe(pluginKey);
});
test("scanText does not flag the key when the plugin is not registered", () => {
expectNoMatch(`The setup doc pastes ${pluginKey} inline`);
});
test("a token ending in '-' is matched in full, including the trailing hyphen", () => {
registerVirlo();
// \b-based wrapping would backtrack past (or miss) the non-word tail.
const hyphenTailKey = "virlo_tkn_Qm7pW2xLbV9sKjR4tNc--";
const input = `key: ${hyphenTailKey} (from setup)`;
const match = expectMatch(input, "Virlo API Key (plugin:virlo)");
expect(input.slice(match.startIndex, match.endIndex)).toBe(hyphenTailKey);
expect(redactSecrets(input)).toBe(
'key: (from setup)',
);
});
test("a key embedded in a longer token-alphabet run is not matched", () => {
registerVirlo();
// Lookarounds treat [A-Za-z0-9_-] as the token alphabet: a key glued to
// surrounding token characters is part of a larger token, not a secret.
expectNoMatch(`prefix${pluginKey}`);
});
test("redactSecrets redacts a registered plugin key", () => {
registerVirlo();
const result = redactSecrets(`run with ${pluginKey} exported`);
expect(result).toBe(
'run with exported',
);
});
test("a second registration mid-test is picked up without restart", () => {
registerVirlo();
expectMatch(`a ${pluginKey} b`, "Virlo API Key (plugin:virlo)");
const acmeKey = `acme_sec_${"Zx9".repeat(8)}`;
registerPluginSecretPatterns("acme", [
{ label: "Acme Token", pattern: "acme_sec_[A-Za-z0-9]{16,}" },
]);
expectMatch(`a ${acmeKey} b`, "Acme Token (plugin:acme)");
// The first plugin's pattern survives the rebuild
expectMatch(`a ${pluginKey} b`, "Virlo API Key (plugin:virlo)");
});
test("unregistering stops detection", () => {
registerVirlo();
expectMatch(`a ${pluginKey} b`, "Virlo API Key (plugin:virlo)");
unregisterPluginSecretPatterns("virlo");
expectNoMatch(`a ${pluginKey} b`);
});
});
// ---------------------------------------------------------------------------
// scanText behavior
// ---------------------------------------------------------------------------
describe("scanText", () => {
test("returns empty array for safe text", () => {
expect(scanText("just normal text with no secrets")).toHaveLength(0);
});
test("returns matches sorted by position", () => {
const input = `second=ghp_${"A".repeat(36)} first=AKIAIOSFODNN7REALKEY`;
const matches = scanText(input);
expect(matches.length).toBeGreaterThanOrEqual(2);
for (let i = 1; i < matches.length; i++) {
expect(matches[i].startIndex).toBeGreaterThanOrEqual(
matches[i - 1].startIndex,
);
}
});
test("does not flag common code patterns", () => {
// git SHA (40 hex chars) should not be flagged as AWS secret
// since it lacks a preceding separator
const sha = "4b825dc642cb6eb9a060e54bf899d15f13fe1d7a";
const matches = scanText(`commit ${sha}`);
const awsMatches = matches.filter((m) => m.type === "AWS Secret Key");
expect(awsMatches).toHaveLength(0);
});
test("handles multi-line input", () => {
const input = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWep4PAtGoSNQ==
-----END RSA PRIVATE KEY-----
`;
expectMatch(input, "Private Key");
});
test("handles empty string", () => {
expect(scanText("")).toHaveLength(0);
});
test("handles very long text without crashing", () => {
const longText = "a".repeat(100_000);
const start = performance.now();
scanText(longText);
const elapsed = performance.now() - start;
// Should complete in under 500ms even for 100KB
expect(elapsed).toBeLessThan(500);
});
test("match includes correct startIndex and endIndex", () => {
const prefix = "key is: ";
const key = "AKIAIOSFODNN7REALKEY";
const input = prefix + key;
const match = expectMatch(input, "AWS Access Key");
expect(match.startIndex).toBe(prefix.length);
expect(match.endIndex).toBe(prefix.length + key.length);
expect(input.slice(match.startIndex, match.endIndex)).toBe(key);
});
});
// ---------------------------------------------------------------------------
// Edge cases / false positives
// ---------------------------------------------------------------------------
describe("false positive resistance", () => {
test("does not flag base64-encoded images", () => {
// A typical short base64 image data chunk — should not trigger
const img = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA";
const matches = scanText(img);
// May match JWT-like patterns; verify no AWS/generic matches
const sensitive = matches.filter(
(m) =>
m.type === "AWS Secret Key" || m.type === "Generic Secret Assignment",
);
expect(sensitive).toHaveLength(0);
});
test("does not flag UUIDs as Heroku keys when they are zero-filled", () => {
const matches = scanText("00000000-0000-0000-0000-000000000000");
const heroku = matches.filter((m) => m.type === "Heroku API Key");
expect(heroku).toHaveLength(0);
});
test("does not flag common hex strings without context", () => {
// MD5/SHA hashes are hex but should not be flagged
expectNoMatch("d41d8cd98f00b204e9800998ecf8427e");
});
test("does not flag public key headers", () => {
const pubKey = "-----BEGIN PUBLIC KEY-----";
const matches = scanText(pubKey);
const privKeys = matches.filter((m) => m.type === "Private Key");
expect(privKeys).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Pass-through cases (allowlist-shaped strings that look secret-ish)
// ---------------------------------------------------------------------------
describe("pass-through behavior", () => {
test("does not redact Telegram invite deep links", () => {
const invite =
"https://t.me/credence_the_bot?start=iv_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789-_ABCDE";
const input = `Here is your invite link: ${invite}`;
const matches = scanText(input);
expect(matches).toHaveLength(0);
expect(redactSecrets(input)).toBe(input);
});
});
// ---------------------------------------------------------------------------
// Overlapping match handling in redactSecrets (#166 feedback)
// ---------------------------------------------------------------------------
describe("overlapping match redaction", () => {
test("does not corrupt output when matches overlap", () => {
// An AWS key inside a generic secret assignment produces overlapping matches
const input = `api_key = "AKIAIOSFODNN7REALKEY"`;
const result = redactSecrets(input);
// Should redact correctly without duplicating markers or losing text
expect(result).not.toContain("AKIAIOSFODNN7REALKEY");
// The outer quotes should be preserved somewhere in the output
expect(result).toContain(' {
// Construct a case where a specific match is entirely inside a broader one
const input = `password = "AKIAIOSFODNN7REALKEY inside text"`;
const result = redactSecrets(input);
// Should have at least one redaction
expect(result).toContain(' {
// A shorter match (e.g. AWS-like 40 chars) inside a longer generic assignment
// should not leak the suffix of the longer match
const input = `password = "AKIAIOSFODNN7REALKEY extra-tail-secret"`;
const result = redactSecrets(input);
// Nothing from the original secret value should leak
expect(result).not.toContain("extra-tail-secret");
expect(result).not.toContain("AKIAIOSFODNN7REALKEY");
expect(result).toContain(' {
// When two matches start at same offset, wider one should be used
const input = `token = "AKIAIOSFODNN7REALKEY-plus-extra-data"`;
const result = redactSecrets(input);
expect(result).not.toContain("AKIAIOSFODNN7REALKEY");
expect(result).not.toContain("plus-extra-data");
expect(result).toContain(' {
test("detects UUID with heroku context keyword", () => {
const uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
const input = `HEROKU_API_KEY=${uuid}`;
const matches = scanText(input);
const heroku = matches.filter((m) => m.type === "Heroku API Key");
expect(heroku).toHaveLength(1);
});
test("detects UUID with heroku_auth_token prefix", () => {
const uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
const input = `heroku_auth_token = "${uuid}"`;
const matches = scanText(input);
const heroku = matches.filter((m) => m.type === "Heroku API Key");
expect(heroku).toHaveLength(1);
});
test("does not flag random UUIDs without heroku context", () => {
const uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
const input = `request_id: ${uuid}`;
const matches = scanText(input);
const heroku = matches.filter((m) => m.type === "Heroku API Key");
expect(heroku).toHaveLength(0);
});
test("does not flag UUIDs in logs", () => {
const input =
"Processed request id=a1b2c3d4-e5f6-7890-abcd-ef1234567890 in 42ms";
const matches = scanText(input);
const heroku = matches.filter((m) => m.type === "Heroku API Key");
expect(heroku).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Unquoted generic secret assignments (#166 feedback)
// ---------------------------------------------------------------------------
describe("unquoted generic secret assignments", () => {
test("detects .env-style unquoted password", () => {
const input = "DATABASE_PASSWORD=supersecret123";
const matches = scanText(input);
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic.length).toBeGreaterThan(0);
});
test("detects .env-style unquoted api key", () => {
const input = "API_KEY=abcdef1234567890";
const matches = scanText(input);
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic.length).toBeGreaterThan(0);
});
test("detects unquoted token assignment", () => {
const input = "AUTH_TOKEN=mysecuretokenvalue123";
const matches = scanText(input);
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic.length).toBeGreaterThan(0);
});
test("still detects quoted assignments", () => {
const input = `secret = "mysupersecretsecret"`;
const matches = scanText(input);
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic.length).toBeGreaterThan(0);
});
test("does not match short unquoted values", () => {
const input = "password=short";
const matches = scanText(input);
const generic = matches.filter(
(m) => m.type === "Generic Secret Assignment",
);
expect(generic).toHaveLength(0);
});
});