import { describe, it, expect } from 'vitest'; import { isSecretPath, filterSecretEntries, SecretBlockedError, DEFAULT_SECURITY_IGNORE_FILETYPES, } from '../secret_guard.js'; // ---- env / config patterns ------------------------------------------------- describe('isSecretPath — env / config files', () => { it('matches .env (literal via .env*)', () => { expect(isSecretPath('.env')).toBe(true); }); it('matches .env.local (via .env*)', () => { expect(isSecretPath('.env.local')).toBe(true); }); it('matches .env.production.local (via .env*)', () => { expect(isSecretPath('.env.production.local')).toBe(true); }); it('matches .envrc (via .env*, common direnv config holding secrets)', () => { expect(isSecretPath('.envrc')).toBe(true); }); it('matches nested .env (apps/server/.env via basename test)', () => { expect(isSecretPath('apps/server/.env')).toBe(true); }); it('case-insensitive: .ENV matches .env*', () => { expect(isSecretPath('.ENV')).toBe(true); }); }); // ---- SSH / cert / key patterns -------------------------------------------- describe('isSecretPath — SSH / certs / keys', () => { it('matches id_rsa (continue.dev literal)', () => { expect(isSecretPath('id_rsa')).toBe(true); }); it('matches id_rsa.pub (BooCode addition id_rsa*)', () => { // continue.dev's literal id_rsa wouldn't match this; BooCode broadens // because .pub files leak hostnames/usernames and authorized_keys hints. expect(isSecretPath('id_rsa.pub')).toBe(true); }); it('matches cert.pem (*.pem)', () => { expect(isSecretPath('cert.pem')).toBe(true); }); it('matches private.key (*.key)', () => { expect(isSecretPath('private.key')).toBe(true); }); }); // ---- credential patterns --------------------------------------------------- describe('isSecretPath — credential files (BooCode additions)', () => { it('matches credentials.json (BooCode *credentials*)', () => { expect(isSecretPath('credentials.json')).toBe(true); }); it('matches aws_credentials (BooCode *credentials* — substring match)', () => { // continue.dev has no `credentials*` pattern. BooCode adds `*credentials*` // to catch the common `aws_credentials`, `gcp-credentials.yml`, etc. expect(isSecretPath('aws_credentials')).toBe(true); }); it('matches .netrc (BooCode addition)', () => { expect(isSecretPath('.netrc')).toBe(true); }); it('matches keystore.kdbx (BooCode addition *.kdbx)', () => { expect(isSecretPath('keystore.kdbx')).toBe(true); }); }); // ---- directory patterns ---------------------------------------------------- describe('isSecretPath — directory segments (trailing-slash patterns)', () => { it('matches files under .aws/ via segment test', () => { expect(isSecretPath('home/user/.aws/credentials')).toBe(true); }); it('matches files under .ssh/', () => { expect(isSecretPath('home/user/.ssh/known_hosts')).toBe(true); }); it('matches files inside any path segment named secrets/', () => { expect(isSecretPath('apps/server/secrets/api.key')).toBe(true); }); }); // ---- negatives ------------------------------------------------------------- describe('isSecretPath — negatives', () => { it('package.json is allowed', () => { expect(isSecretPath('package.json')).toBe(false); }); it('README.md is allowed', () => { expect(isSecretPath('README.md')).toBe(false); }); it('Login.tsx is allowed (substring "login" doesn\'t trigger anything)', () => { expect(isSecretPath('src/components/Login.tsx')).toBe(false); }); it('empty string returns false (defensive)', () => { expect(isSecretPath('')).toBe(false); }); it('a directory NAMED "credentials" alone does NOT trigger — only file basenames do', () => { // Worth pinning: BooCode's `*credentials*` is a basename pattern (no // trailing `/`), so it tests the leaf filename only. A directory // literally called "credentials" containing innocuous files (e.g. // Login.tsx) is fine. This is a deliberate trade-off vs. continue.dev's // dir-pattern approach — adding `credentials/` as a dir pattern would // block legitimate code like `src/auth/credentials/Login.tsx`. expect(isSecretPath('src/auth/credentials/Login.tsx')).toBe(false); // ...but a file INSIDE that dir whose name includes "credentials" still // blocks via the basename match: expect(isSecretPath('src/auth/credentials/credentials.ts')).toBe(true); }); }); // ---- filterSecretEntries (listing-tools helper) ---------------------------- describe('filterSecretEntries', () => { it('removes secret entries and reports the count via note string', () => { const entries = [ { path: 'src/index.ts' }, { path: '.env' }, { path: 'README.md' }, { path: 'id_rsa' }, { path: 'apps/server/package.json' }, ]; const result = filterSecretEntries(entries, (e) => e.path); expect(result.kept.map((e) => e.path)).toEqual([ 'src/index.ts', 'README.md', 'apps/server/package.json', ]); expect(result.hidden).toBe(2); expect(result.note).toBe('[pathGuard: 2 entries hidden by secret-file filter]'); }); it('returns undefined note when nothing was filtered', () => { const result = filterSecretEntries( [{ path: 'a.ts' }, { path: 'b.ts' }], (e) => e.path, ); expect(result.kept).toHaveLength(2); expect(result.hidden).toBe(0); expect(result.note).toBeUndefined(); }); it('uses singular "entry" for a 1-hit filter (cosmetic but worth pinning)', () => { const result = filterSecretEntries( [{ path: 'index.ts' }, { path: '.env' }], (e) => e.path, ); expect(result.note).toBe('[pathGuard: 1 entry hidden by secret-file filter]'); }); }); // ---- SecretBlockedError ---------------------------------------------------- describe('SecretBlockedError', () => { it('carries the offending path on .path and in the message', () => { const err = new SecretBlockedError('apps/server/.env'); expect(err.name).toBe('SecretBlockedError'); expect(err.path).toBe('apps/server/.env'); expect(err.message).toContain('apps/server/.env'); expect(err.message).toContain('pathGuard'); }); }); // ---- contract sanity check ------------------------------------------------- describe('DEFAULT_SECURITY_IGNORE_FILETYPES', () => { it('exports at least 40 patterns (continue.dev base) and is non-empty', () => { expect(DEFAULT_SECURITY_IGNORE_FILETYPES.length).toBeGreaterThanOrEqual(40); }); it('includes all the headline continue.dev entries we tested above', () => { // Spot-check that the list still carries the patterns whose behavior // the tests depend on. Catches an accidental list edit that would // silently degrade coverage. const set = new Set(DEFAULT_SECURITY_IGNORE_FILETYPES); for (const pat of ['*.env', '.env*', '*.pem', '*.key', 'id_rsa', '.aws/', '.ssh/']) { expect(set.has(pat), `missing pattern: ${pat}`).toBe(true); } }); });