From 132e27e6f0c3ebd6f43078521714cb70de84263f Mon Sep 17 00:00:00 2001 From: boulea7 Date: Sat, 10 Oct 2026 07:45:26 +0800 Subject: [PATCH] Exclude paths ending in .env by default --- README.md | 6 ++ packages/core/src/filesystem.ts | 2 +- packages/core/test/filesystem.test.ts | 108 ++++++++++++++++++++++++++ test/retrieval.test.ts | 107 +++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 63f3422..b275dab 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,12 @@ filtering flags as search. To skip paths inside that root for one search, pass `--exclude` with a gitignore pattern relative to the root, for example `--exclude '**/*.test.ts' --exclude 'src/generated/'`. +The default credential-name filter excludes `.env`, `.env.*`, and path components +ending in `.env`, case-insensitively. This includes files such as `keys.env` and +directories such as `samples.env` and their contents. Use `--include-sensitive` +to include them; hidden paths also need `--hidden`. Explicit `--exclude` patterns +still apply. + `jg auth` asks for your provider, then saves its key in an owner-only config file. Re-running auth replaces that setup; searches always use the saved provider. `jg doctor` checks it with synthetic input. Existing saved keys without a provider diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 014b431..a9fc7bb 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -62,7 +62,7 @@ export const filesystemDefaults = Object.freeze({ ".npmrc", ".pypirc", ] as readonly string[], - sensitiveSuffixes: [".pem", ".key", ".p12", ".pfx"] as readonly string[], + sensitiveSuffixes: [".env", ".pem", ".key", ".p12", ".pfx"] as readonly string[], pageSize: 128, maxOpenDirectories: 64, maxFileBytes: 16 * 1024 * 1024, diff --git a/packages/core/test/filesystem.test.ts b/packages/core/test/filesystem.test.ts index d821e50..6b51831 100644 --- a/packages/core/test/filesystem.test.ts +++ b/packages/core/test/filesystem.test.ts @@ -19,6 +19,114 @@ async function fixture(files: Record) { return root; } +test("env-suffixed files are excluded case-insensitively at any depth", async () => { + const root = await fixture({ + "keys.env": "ROOT_ENV_FIXTURE", + "nested/prod.env": "NESTED_ENV_FIXTURE", + "nested/foo.ENV": "UPPERCASE_ENV_FIXTURE", + "source.ts": "export const visible = true;\n", + }); + const reader = await createFilesystem({ root }); + try { + for (const path of ["keys.env", "nested/prod.env", "nested/foo.ENV"]) + expect(await reader.readSnapshot(path)).toMatchObject({ + status: "excluded", + reason: "sensitive_name", + }); + expect((await reader.listPage()).entries.map((entry) => entry.path).sort()).toEqual([ + "nested", + "source.ts", + ]); + expect((await reader.listPage("nested")).entries).toEqual([]); + } finally { + await reader.close(); + } +}); + +test("hidden env files require independent hidden and sensitive overrides", async () => { + const root = await fixture({ + ".env": "DOT_ENV_FIXTURE", + ".env.local": "LOCAL_ENV_FIXTURE", + "keys.env": "SUFFIX_ENV_FIXTURE", + }); + for (const policy of [ + {}, + { hidden: true }, + { includeSensitive: true }, + { hidden: true, includeSensitive: true }, + ]) { + const reader = await createFilesystem({ root, policy }); + try { + expect((await reader.readSnapshot("keys.env")).status).toBe( + "includeSensitive" in policy ? "ok" : "excluded", + ); + for (const path of [".env", ".env.local"]) + expect((await reader.readSnapshot(path)).status).toBe( + "hidden" in policy && "includeSensitive" in policy ? "ok" : "excluded", + ); + } finally { + await reader.close(); + } + } +}); + +test("env-like and ordinary configuration filenames remain eligible", async () => { + const paths = [ + "keys.env.example", + "config.env.json", + "settings.json", + "config.toml", + "keys.json", + ]; + const root = await fixture( + Object.fromEntries(paths.map((path) => [path, "CONFIGURATION_FIXTURE"])), + ); + const reader = await createFilesystem({ root }); + try { + for (const path of paths) + expect(await reader.readSnapshot(path)).toMatchObject({ + status: "ok", + snapshot: { path, source: "CONFIGURATION_FIXTURE" }, + }); + expect((await reader.listPage()).entries.map((entry) => entry.path).sort()).toEqual( + paths.sort(), + ); + } finally { + await reader.close(); + } +}); + +test("env-suffixed directories are pruned unless sensitive paths are included", async () => { + const root = await fixture({ + "samples.env/fixture.ts": "export const sample = true;\n", + "source.ts": "export const visible = true;\n", + }); + const reader = await createFilesystem({ root }); + try { + expect((await reader.listPage()).entries.map((entry) => entry.path)).toEqual(["source.ts"]); + expect((await reader.listPage("samples.env")).entries).toEqual([]); + expect(await reader.readSnapshot("samples.env/fixture.ts")).toMatchObject({ + status: "excluded", + reason: "sensitive_name", + }); + } finally { + await reader.close(); + } + const included = await createFilesystem({ root, policy: { includeSensitive: true } }); + try { + expect((await included.listPage()).entries.map((entry) => entry.path).sort()).toEqual([ + "samples.env", + "source.ts", + ]); + expect(await included.readSnapshot("samples.env/fixture.ts")).toMatchObject({ + status: "ok", + snapshot: { path: "samples.env/fixture.ts", source: "export const sample = true;\n" }, + }); + } finally { + await included.close(); + } +}); + test("one reader excludes ignored, hidden, dependent, sensitive and binary content from plain roots", async () => { const root = await fixture({ ".gitignore": "ignored.txt\n", diff --git a/test/retrieval.test.ts b/test/retrieval.test.ts index 4c02a17..cacbd05 100644 --- a/test/retrieval.test.ts +++ b/test/retrieval.test.ts @@ -8,6 +8,113 @@ import { testIfDocker } from "./helpers/docker"; import { retrieve } from "../packages/core/src/retrieve"; import { createEvaluator, EvaluationFailure } from "../packages/core/src/evaluator"; +testIfDocker( + "env-suffixed files never reach the provider by default", + async () => { + const root = await mkdtemp(join(tmpdir(), "jg-env-suffix-")); + const sent: string[] = []; + const server = Bun.serve({ + port: 0, + async fetch(request) { + const body = (await request.json()) as { questions: Record }; + sent.push(JSON.stringify(body)); + return Response.json({ + answers: Object.fromEntries( + Object.keys(body.questions).map((id) => [id, { type: "noul", noul: 0.9 }]), + ), + }); + }, + }); + try { + await writeFile(join(root, "keys.env"), "FIXTURE_VALUE=ENV_SUFFIX_EXCLUDED_FILE_MARKER\n"); + await writeFile( + join(root, "source.ts"), + 'export const sourceMarker = "ENV_SUFFIX_NORMAL_SOURCE_MARKER";\n', + ); + const signal = new AbortController().signal; + const evaluator = createEvaluator({ + apiKey: "fixture", + provider: "vercel", + fetch: routeProviderFetch(fetch, `http://127.0.0.1:${server.port}`), + signal, + }); + const result = await retrieve({ root, query: "find fixture source", signal }, evaluator); + expect(result.status).toBe("complete"); + expect(sent.length).toBeGreaterThan(0); + expect(result.counts.requests).toBe(sent.length); + expect(sent.join("\n")).toContain("ENV_SUFFIX_NORMAL_SOURCE_MARKER"); + expect(sent.join("\n")).not.toContain("ENV_SUFFIX_EXCLUDED_FILE_MARKER"); + expect(result.files.map((file) => file.path)).toEqual(["source.ts"]); + } finally { + server.stop(true); + await rm(root, { recursive: true, force: true }); + } + }, + 120_000, +); + +testIfDocker( + "sensitive overrides admit env suffixes without bypassing explicit excludes", + async () => { + const root = await mkdtemp(join(tmpdir(), "jg-env-override-")); + const sent: string[] = []; + const server = Bun.serve({ + port: 0, + async fetch(request) { + const body = (await request.json()) as { questions: Record }; + sent.push(JSON.stringify(body)); + return Response.json({ + answers: Object.fromEntries( + Object.keys(body.questions).map((id) => [id, { type: "noul", noul: 0.9 }]), + ), + }); + }, + }); + try { + await writeFile(join(root, "keys.env"), "FIXTURE_VALUE=ENV_SUFFIX_OVERRIDE_MARKER\n"); + for (const path of [".env", ".env.local"]) + await writeFile(join(root, path), "FIXTURE_VALUE=ENV_SUFFIX_HIDDEN_MARKER\n"); + await writeFile( + join(root, "source.ts"), + 'export const sourceMarker = "ENV_OVERRIDE_NORMAL_SOURCE_MARKER";\n', + ); + for (const policy of [ + { includeSensitive: true }, + { includeSensitive: true, exclude: ["keys.env"] }, + ]) { + sent.length = 0; + const signal = new AbortController().signal; + const evaluator = createEvaluator({ + apiKey: "fixture", + provider: "vercel", + fetch: routeProviderFetch(fetch, `http://127.0.0.1:${server.port}`), + signal, + }); + const result = await retrieve( + { root, query: "find fixture source", policy, signal }, + evaluator, + ); + expect(result.status).toBe("complete"); + expect(sent.length).toBeGreaterThan(0); + expect(result.counts.requests).toBe(sent.length); + expect(sent.join("\n")).toContain("ENV_OVERRIDE_NORMAL_SOURCE_MARKER"); + expect(sent.join("\n")).not.toContain("ENV_SUFFIX_HIDDEN_MARKER"); + if (policy.exclude) { + expect(sent.join("\n")).not.toContain("ENV_SUFFIX_OVERRIDE_MARKER"); + expect(result.files.map((file) => file.path)).toEqual(["source.ts"]); + } else { + expect(sent.join("\n")).toContain("ENV_SUFFIX_OVERRIDE_MARKER"); + expect(result.files.map((file) => file.path).sort()).toEqual(["keys.env", "source.ts"]); + } + } + } finally { + server.stop(true); + await rm(root, { recursive: true, force: true }); + } + }, + 120_000, +); + testIfDocker( "hierarchical retrieval returns source from multiple files without uploading excluded data", async () => {