env-guardrail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ // src/scanner.ts
2
+ import fs from "fs";
3
+ import { glob } from "glob";
4
+ import { parse } from "@babel/parser";
5
+ import traverseModule from "@babel/traverse";
6
+ var traverse = traverseModule.default || traverseModule;
7
+ function scanCodebase(rootDir = ".") {
8
+ const files = glob.sync("**/*.{js,jsx,ts,tsx}", {
9
+ cwd: rootDir,
10
+ ignore: ["node_modules/**", "dist/**", ".next/**", "build/**"]
11
+ });
12
+ const discovered = [];
13
+ for (const file of files) {
14
+ try {
15
+ const code = fs.readFileSync(file, "utf-8");
16
+ const ast = parse(code, {
17
+ sourceType: "module",
18
+ plugins: ["typescript", "jsx"]
19
+ });
20
+ traverse(ast, {
21
+ MemberExpression(path) {
22
+ const node = path.node;
23
+ if (node.object.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env" && node.property.type === "Identifier") {
24
+ discovered.push({
25
+ key: node.property.name,
26
+ filePath: file,
27
+ line: node.loc?.start.line || 0
28
+ });
29
+ }
30
+ if (node.object.type === "MemberExpression" && node.object.object?.type === "MetaProperty" && node.object.property?.name === "env" && node.property.type === "Identifier") {
31
+ discovered.push({
32
+ key: node.property.name,
33
+ filePath: file,
34
+ line: node.loc?.start.line || 0
35
+ });
36
+ }
37
+ }
38
+ });
39
+ } catch {
40
+ }
41
+ }
42
+ return discovered;
43
+ }
44
+
45
+ // src/envParser.ts
46
+ import fs2 from "fs";
47
+ import dotenv from "dotenv";
48
+ var SECRET_PATTERNS = [
49
+ /sk_live_[0-9a-zA-Z]{24,}/,
50
+ /AKIA[0-9A-Z]{16}/,
51
+ /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,
52
+ /-----BEGIN PRIVATE KEY-----/
53
+ ];
54
+ function parseEnvFile(envPath = ".env") {
55
+ const map = /* @__PURE__ */ new Map();
56
+ if (!fs2.existsSync(envPath)) return map;
57
+ const content = fs2.readFileSync(envPath, "utf-8");
58
+ const parsed = dotenv.parse(content);
59
+ for (const [key, value] of Object.entries(parsed)) {
60
+ const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));
61
+ map.set(key, { key, value, isPotentialSecret: isSecret });
62
+ }
63
+ return map;
64
+ }
65
+
66
+ export {
67
+ scanCodebase,
68
+ parseEnvFile
69
+ };
70
+ //# sourceMappingURL=chunk-636KQXX3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scanner.ts","../src/envParser.ts"],"sourcesContent":["import fs from 'node:fs';\nimport {glob}from 'glob';\nimport { parse } from '@babel/parser';\nimport traverseModule from '@babel/traverse';\n\nconst traverse = (traverseModule as any).default || traverseModule;\n\nexport interface DiscoveredKey {\n key: string;\n filePath: string;\n line: number;\n}\n\nexport function scanCodebase(rootDir = '.'): DiscoveredKey[] {\n const files = glob.sync('**/*.{js,jsx,ts,tsx}', {\n cwd: rootDir,\n ignore: ['node_modules/**', 'dist/**', '.next/**', 'build/**'],\n });\n\n const discovered: DiscoveredKey[] = [];\n\n for (const file of files) {\n try {\n const code = fs.readFileSync(file, 'utf-8');\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n });\n\n traverse(ast, {\n MemberExpression(path: any) {\n const node = path.node;\n // Detect process.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.name === 'process' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n // Detect import.meta.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.type === 'MetaProperty' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n },\n });\n } catch {\n // Ignore unparseable or non-JS files\n }\n }\n\n return discovered;\n}","import fs from 'node:fs';\nimport dotenv from 'dotenv';\n\nexport interface EnvVariable {\n key: string;\n value: string;\n isPotentialSecret: boolean;\n}\n\n// High-risk pattern detector (Stripe, AWS, JWT, generic private keys)\nconst SECRET_PATTERNS = [\n /sk_live_[0-9a-zA-Z]{24,}/,\n /AKIA[0-9A-Z]{16}/,\n /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,\n /-----BEGIN PRIVATE KEY-----/,\n];\n\nexport function parseEnvFile(envPath = '.env'): Map<string, EnvVariable> {\n const map = new Map<string, EnvVariable>();\n if (!fs.existsSync(envPath)) return map;\n\n const content = fs.readFileSync(envPath, 'utf-8');\n const parsed = dotenv.parse(content);\n\n for (const [key, value] of Object.entries(parsed)) {\n const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));\n map.set(key, { key, value, isPotentialSecret: isSecret });\n }\n\n return map;\n}"],"mappings":";AAAA,OAAO,QAAQ;AACf,SAAQ,YAAU;AAClB,SAAS,aAAa;AACtB,OAAO,oBAAoB;AAE3B,IAAM,WAAY,eAAuB,WAAW;AAQ7C,SAAS,aAAa,UAAU,KAAsB;AAC3D,QAAM,QAAQ,KAAK,KAAK,wBAAwB;AAAA,IAC9C,KAAK;AAAA,IACL,QAAQ,CAAC,mBAAmB,WAAW,YAAY,UAAU;AAAA,EAC/D,CAAC;AAED,QAAM,aAA8B,CAAC;AAErC,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,YAAM,OAAO,GAAG,aAAa,MAAM,OAAO;AAC1C,YAAM,MAAM,MAAM,MAAM;AAAA,QACtB,YAAY;AAAA,QACZ,SAAS,CAAC,cAAc,KAAK;AAAA,MAC/B,CAAC;AAED,eAAS,KAAK;AAAA,QACZ,iBAAiB,MAAW;AAC1B,gBAAM,OAAO,KAAK;AAElB,cACE,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,QAAQ,SAAS,aAC7B,KAAK,OAAO,UAAU,SAAS,SAC/B,KAAK,SAAS,SAAS,cACvB;AACA,uBAAW,KAAK;AAAA,cACd,KAAK,KAAK,SAAS;AAAA,cACnB,UAAU;AAAA,cACV,MAAM,KAAK,KAAK,MAAM,QAAQ;AAAA,YAChC,CAAC;AAAA,UACH;AAEA,cACE,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,QAAQ,SAAS,kBAC7B,KAAK,OAAO,UAAU,SAAS,SAC/B,KAAK,SAAS,SAAS,cACvB;AACA,uBAAW,KAAK;AAAA,cACd,KAAK,KAAK,SAAS;AAAA,cACnB,UAAU;AAAA,cACV,MAAM,KAAK,KAAK,MAAM,QAAQ;AAAA,YAChC,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;;;AClEA,OAAOA,SAAQ;AACf,OAAO,YAAY;AASnB,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,aAAa,UAAU,QAAkC;AACvE,QAAM,MAAM,oBAAI,IAAyB;AACzC,MAAI,CAACA,IAAG,WAAW,OAAO,EAAG,QAAO;AAEpC,QAAM,UAAUA,IAAG,aAAa,SAAS,OAAO;AAChD,QAAM,SAAS,OAAO,MAAM,OAAO;AAEnC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,WAAW,gBAAgB,KAAK,CAAC,YAAY,QAAQ,KAAK,KAAK,CAAC;AACtE,QAAI,IAAI,KAAK,EAAE,KAAK,OAAO,mBAAmB,SAAS,CAAC;AAAA,EAC1D;AAEA,SAAO;AACT;","names":["fs"]}
package/dist/cli.cjs ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+ var import_picocolors = __toESM(require("picocolors"), 1);
29
+
30
+ // src/scanner.ts
31
+ var import_node_fs = __toESM(require("fs"), 1);
32
+ var import_glob = require("glob");
33
+ var import_parser = require("@babel/parser");
34
+ var import_traverse = __toESM(require("@babel/traverse"), 1);
35
+ var traverse = import_traverse.default.default || import_traverse.default;
36
+ function scanCodebase(rootDir = ".") {
37
+ const files = import_glob.glob.sync("**/*.{js,jsx,ts,tsx}", {
38
+ cwd: rootDir,
39
+ ignore: ["node_modules/**", "dist/**", ".next/**", "build/**"]
40
+ });
41
+ const discovered = [];
42
+ for (const file of files) {
43
+ try {
44
+ const code = import_node_fs.default.readFileSync(file, "utf-8");
45
+ const ast = (0, import_parser.parse)(code, {
46
+ sourceType: "module",
47
+ plugins: ["typescript", "jsx"]
48
+ });
49
+ traverse(ast, {
50
+ MemberExpression(path) {
51
+ const node = path.node;
52
+ if (node.object.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env" && node.property.type === "Identifier") {
53
+ discovered.push({
54
+ key: node.property.name,
55
+ filePath: file,
56
+ line: node.loc?.start.line || 0
57
+ });
58
+ }
59
+ if (node.object.type === "MemberExpression" && node.object.object?.type === "MetaProperty" && node.object.property?.name === "env" && node.property.type === "Identifier") {
60
+ discovered.push({
61
+ key: node.property.name,
62
+ filePath: file,
63
+ line: node.loc?.start.line || 0
64
+ });
65
+ }
66
+ }
67
+ });
68
+ } catch {
69
+ }
70
+ }
71
+ return discovered;
72
+ }
73
+
74
+ // src/envParser.ts
75
+ var import_node_fs2 = __toESM(require("fs"), 1);
76
+ var import_dotenv = __toESM(require("dotenv"), 1);
77
+ var SECRET_PATTERNS = [
78
+ /sk_live_[0-9a-zA-Z]{24,}/,
79
+ /AKIA[0-9A-Z]{16}/,
80
+ /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,
81
+ /-----BEGIN PRIVATE KEY-----/
82
+ ];
83
+ function parseEnvFile(envPath = ".env") {
84
+ const map = /* @__PURE__ */ new Map();
85
+ if (!import_node_fs2.default.existsSync(envPath)) return map;
86
+ const content = import_node_fs2.default.readFileSync(envPath, "utf-8");
87
+ const parsed = import_dotenv.default.parse(content);
88
+ for (const [key, value] of Object.entries(parsed)) {
89
+ const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));
90
+ map.set(key, { key, value, isPotentialSecret: isSecret });
91
+ }
92
+ return map;
93
+ }
94
+
95
+ // src/cli.ts
96
+ var program = new import_commander.Command();
97
+ program.name("env-guardrail").description("Audit environment variables against codebase usage").version("0.1.0");
98
+ program.command("check").option("-e, --env <path>", "Path to target .env file", ".env").action((options) => {
99
+ console.log(import_picocolors.default.cyan("\n\u{1F50D} Scanning codebase for environment variables...\n"));
100
+ const codeUsage = scanCodebase(".");
101
+ const envVariables = parseEnvFile(options.env);
102
+ const usedKeys = new Set(codeUsage.map((u) => u.key));
103
+ let errorCount = 0;
104
+ let warningCount = 0;
105
+ for (const usage of codeUsage) {
106
+ if (!envVariables.has(usage.key)) {
107
+ console.log(
108
+ `${import_picocolors.default.red("\u2716 MISSING KEY")} ${import_picocolors.default.bold(usage.key)}
109
+ Found in: ${import_picocolors.default.dim(`${usage.filePath}:${usage.line}`)}
110
+ Action: Define this variable in ${options.env}
111
+ `
112
+ );
113
+ errorCount++;
114
+ }
115
+ }
116
+ for (const [key] of envVariables.entries()) {
117
+ if (!usedKeys.has(key)) {
118
+ console.log(
119
+ `${import_picocolors.default.yellow("\u26A0 UNUSED KEY")} ${import_picocolors.default.bold(key)}
120
+ Defined in: ${import_picocolors.default.dim(options.env)}
121
+ Action: No references found in codebase.
122
+ `
123
+ );
124
+ warningCount++;
125
+ }
126
+ }
127
+ for (const [key, variable] of envVariables.entries()) {
128
+ if (variable.isPotentialSecret) {
129
+ console.log(
130
+ `${import_picocolors.default.red("\u2716 SECRET LEAK")} ${import_picocolors.default.bold(key)}
131
+ Defined in: ${import_picocolors.default.dim(options.env)}
132
+ Action: Value looks like an unmasked production secret key.
133
+ `
134
+ );
135
+ errorCount++;
136
+ }
137
+ }
138
+ console.log(import_picocolors.default.dim("--------------------------------------------------"));
139
+ if (errorCount === 0 && warningCount === 0) {
140
+ console.log(import_picocolors.default.green("\u2714 All environment variables are consistent! No leaks detected.\n"));
141
+ process.exit(0);
142
+ } else {
143
+ console.log(
144
+ `Result: ${import_picocolors.default.red(`${errorCount} error(s)`)}, ${import_picocolors.default.yellow(`${warningCount} warning(s)`)}
145
+ `
146
+ );
147
+ if (errorCount > 0) process.exit(1);
148
+ }
149
+ });
150
+ program.parse(process.argv);
151
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/scanner.ts","../src/envParser.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport pc from 'picocolors';\nimport { scanCodebase } from './scanner.js';\nimport { parseEnvFile } from './envParser.js';\n\nconst program = new Command();\n\nprogram\n .name('env-guardrail')\n .description('Audit environment variables against codebase usage')\n .version('0.1.0');\n\nprogram\n .command('check')\n .option('-e, --env <path>', 'Path to target .env file', '.env')\n .action((options) => {\n console.log(pc.cyan('\\n🔍 Scanning codebase for environment variables...\\n'));\n\n const codeUsage = scanCodebase('.');\n const envVariables = parseEnvFile(options.env);\n\n const usedKeys = new Set(codeUsage.map((u) => u.key));\n let errorCount = 0;\n let warningCount = 0;\n\n // 1. Check for Missing Variables\n for (const usage of codeUsage) {\n if (!envVariables.has(usage.key)) {\n console.log(\n `${pc.red('✖ MISSING KEY')} ${pc.bold(usage.key)}\\n` +\n ` Found in: ${pc.dim(`${usage.filePath}:${usage.line}`)}\\n` +\n ` Action: Define this variable in ${options.env}\\n`\n );\n errorCount++;\n }\n }\n\n // 2. Check for Unused Variables\n for (const [key] of envVariables.entries()) {\n if (!usedKeys.has(key)) {\n console.log(\n `${pc.yellow('⚠ UNUSED KEY')} ${pc.bold(key)}\\n` +\n ` Defined in: ${pc.dim(options.env)}\\n` +\n ` Action: No references found in codebase.\\n`\n );\n warningCount++;\n }\n }\n\n // 3. Check for Secret Leaks\n for (const [key, variable] of envVariables.entries()) {\n if (variable.isPotentialSecret) {\n console.log(\n `${pc.red('✖ SECRET LEAK')} ${pc.bold(key)}\\n` +\n ` Defined in: ${pc.dim(options.env)}\\n` +\n ` Action: Value looks like an unmasked production secret key.\\n`\n );\n errorCount++;\n }\n }\n\n // Summary output\n console.log(pc.dim('--------------------------------------------------'));\n if (errorCount === 0 && warningCount === 0) {\n console.log(pc.green('✔ All environment variables are consistent! No leaks detected.\\n'));\n process.exit(0);\n } else {\n console.log(\n `Result: ${pc.red(`${errorCount} error(s)`)}, ${pc.yellow(`${warningCount} warning(s)`)}\\n`\n );\n if (errorCount > 0) process.exit(1);\n }\n });\n\nprogram.parse(process.argv);","import fs from 'node:fs';\nimport {glob}from 'glob';\nimport { parse } from '@babel/parser';\nimport traverseModule from '@babel/traverse';\n\nconst traverse = (traverseModule as any).default || traverseModule;\n\nexport interface DiscoveredKey {\n key: string;\n filePath: string;\n line: number;\n}\n\nexport function scanCodebase(rootDir = '.'): DiscoveredKey[] {\n const files = glob.sync('**/*.{js,jsx,ts,tsx}', {\n cwd: rootDir,\n ignore: ['node_modules/**', 'dist/**', '.next/**', 'build/**'],\n });\n\n const discovered: DiscoveredKey[] = [];\n\n for (const file of files) {\n try {\n const code = fs.readFileSync(file, 'utf-8');\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n });\n\n traverse(ast, {\n MemberExpression(path: any) {\n const node = path.node;\n // Detect process.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.name === 'process' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n // Detect import.meta.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.type === 'MetaProperty' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n },\n });\n } catch {\n // Ignore unparseable or non-JS files\n }\n }\n\n return discovered;\n}","import fs from 'node:fs';\nimport dotenv from 'dotenv';\n\nexport interface EnvVariable {\n key: string;\n value: string;\n isPotentialSecret: boolean;\n}\n\n// High-risk pattern detector (Stripe, AWS, JWT, generic private keys)\nconst SECRET_PATTERNS = [\n /sk_live_[0-9a-zA-Z]{24,}/,\n /AKIA[0-9A-Z]{16}/,\n /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,\n /-----BEGIN PRIVATE KEY-----/,\n];\n\nexport function parseEnvFile(envPath = '.env'): Map<string, EnvVariable> {\n const map = new Map<string, EnvVariable>();\n if (!fs.existsSync(envPath)) return map;\n\n const content = fs.readFileSync(envPath, 'utf-8');\n const parsed = dotenv.parse(content);\n\n for (const [key, value] of Object.entries(parsed)) {\n const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));\n map.set(key, { key, value, isPotentialSecret: isSecret });\n }\n\n return map;\n}"],"mappings":"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","names":["traverseModule","fs","import_node_fs","fs","dotenv","pc"]}
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseEnvFile,
4
+ scanCodebase
5
+ } from "./chunk-636KQXX3.js";
6
+
7
+ // src/cli.ts
8
+ import { Command } from "commander";
9
+ import pc from "picocolors";
10
+ var program = new Command();
11
+ program.name("env-guardrail").description("Audit environment variables against codebase usage").version("0.1.0");
12
+ program.command("check").option("-e, --env <path>", "Path to target .env file", ".env").action((options) => {
13
+ console.log(pc.cyan("\n\u{1F50D} Scanning codebase for environment variables...\n"));
14
+ const codeUsage = scanCodebase(".");
15
+ const envVariables = parseEnvFile(options.env);
16
+ const usedKeys = new Set(codeUsage.map((u) => u.key));
17
+ let errorCount = 0;
18
+ let warningCount = 0;
19
+ for (const usage of codeUsage) {
20
+ if (!envVariables.has(usage.key)) {
21
+ console.log(
22
+ `${pc.red("\u2716 MISSING KEY")} ${pc.bold(usage.key)}
23
+ Found in: ${pc.dim(`${usage.filePath}:${usage.line}`)}
24
+ Action: Define this variable in ${options.env}
25
+ `
26
+ );
27
+ errorCount++;
28
+ }
29
+ }
30
+ for (const [key] of envVariables.entries()) {
31
+ if (!usedKeys.has(key)) {
32
+ console.log(
33
+ `${pc.yellow("\u26A0 UNUSED KEY")} ${pc.bold(key)}
34
+ Defined in: ${pc.dim(options.env)}
35
+ Action: No references found in codebase.
36
+ `
37
+ );
38
+ warningCount++;
39
+ }
40
+ }
41
+ for (const [key, variable] of envVariables.entries()) {
42
+ if (variable.isPotentialSecret) {
43
+ console.log(
44
+ `${pc.red("\u2716 SECRET LEAK")} ${pc.bold(key)}
45
+ Defined in: ${pc.dim(options.env)}
46
+ Action: Value looks like an unmasked production secret key.
47
+ `
48
+ );
49
+ errorCount++;
50
+ }
51
+ }
52
+ console.log(pc.dim("--------------------------------------------------"));
53
+ if (errorCount === 0 && warningCount === 0) {
54
+ console.log(pc.green("\u2714 All environment variables are consistent! No leaks detected.\n"));
55
+ process.exit(0);
56
+ } else {
57
+ console.log(
58
+ `Result: ${pc.red(`${errorCount} error(s)`)}, ${pc.yellow(`${warningCount} warning(s)`)}
59
+ `
60
+ );
61
+ if (errorCount > 0) process.exit(1);
62
+ }
63
+ });
64
+ program.parse(process.argv);
65
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport pc from 'picocolors';\nimport { scanCodebase } from './scanner.js';\nimport { parseEnvFile } from './envParser.js';\n\nconst program = new Command();\n\nprogram\n .name('env-guardrail')\n .description('Audit environment variables against codebase usage')\n .version('0.1.0');\n\nprogram\n .command('check')\n .option('-e, --env <path>', 'Path to target .env file', '.env')\n .action((options) => {\n console.log(pc.cyan('\\n🔍 Scanning codebase for environment variables...\\n'));\n\n const codeUsage = scanCodebase('.');\n const envVariables = parseEnvFile(options.env);\n\n const usedKeys = new Set(codeUsage.map((u) => u.key));\n let errorCount = 0;\n let warningCount = 0;\n\n // 1. Check for Missing Variables\n for (const usage of codeUsage) {\n if (!envVariables.has(usage.key)) {\n console.log(\n `${pc.red('✖ MISSING KEY')} ${pc.bold(usage.key)}\\n` +\n ` Found in: ${pc.dim(`${usage.filePath}:${usage.line}`)}\\n` +\n ` Action: Define this variable in ${options.env}\\n`\n );\n errorCount++;\n }\n }\n\n // 2. Check for Unused Variables\n for (const [key] of envVariables.entries()) {\n if (!usedKeys.has(key)) {\n console.log(\n `${pc.yellow('⚠ UNUSED KEY')} ${pc.bold(key)}\\n` +\n ` Defined in: ${pc.dim(options.env)}\\n` +\n ` Action: No references found in codebase.\\n`\n );\n warningCount++;\n }\n }\n\n // 3. Check for Secret Leaks\n for (const [key, variable] of envVariables.entries()) {\n if (variable.isPotentialSecret) {\n console.log(\n `${pc.red('✖ SECRET LEAK')} ${pc.bold(key)}\\n` +\n ` Defined in: ${pc.dim(options.env)}\\n` +\n ` Action: Value looks like an unmasked production secret key.\\n`\n );\n errorCount++;\n }\n }\n\n // Summary output\n console.log(pc.dim('--------------------------------------------------'));\n if (errorCount === 0 && warningCount === 0) {\n console.log(pc.green('✔ All environment variables are consistent! No leaks detected.\\n'));\n process.exit(0);\n } else {\n console.log(\n `Result: ${pc.red(`${errorCount} error(s)`)}, ${pc.yellow(`${warningCount} warning(s)`)}\\n`\n );\n if (errorCount > 0) process.exit(1);\n }\n });\n\nprogram.parse(process.argv);"],"mappings":";;;;;;;AACA,SAAS,eAAe;AACxB,OAAO,QAAQ;AAIf,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,eAAe,EACpB,YAAY,oDAAoD,EAChE,QAAQ,OAAO;AAElB,QACG,QAAQ,OAAO,EACf,OAAO,oBAAoB,4BAA4B,MAAM,EAC7D,OAAO,CAAC,YAAY;AACnB,UAAQ,IAAI,GAAG,KAAK,8DAAuD,CAAC;AAE5E,QAAM,YAAY,aAAa,GAAG;AAClC,QAAM,eAAe,aAAa,QAAQ,GAAG;AAE7C,QAAM,WAAW,IAAI,IAAI,UAAU,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AACpD,MAAI,aAAa;AACjB,MAAI,eAAe;AAGnB,aAAW,SAAS,WAAW;AAC7B,QAAI,CAAC,aAAa,IAAI,MAAM,GAAG,GAAG;AAChC,cAAQ;AAAA,QACN,GAAG,GAAG,IAAI,oBAAe,CAAC,KAAK,GAAG,KAAK,MAAM,GAAG,CAAC;AAAA,2BACrB,GAAG,IAAI,GAAG,MAAM,QAAQ,IAAI,MAAM,IAAI,EAAE,CAAC;AAAA,iDACnB,QAAQ,GAAG;AAAA;AAAA,MAC/D;AACA;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,GAAG,KAAK,aAAa,QAAQ,GAAG;AAC1C,QAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,cAAQ;AAAA,QACN,GAAG,GAAG,OAAO,mBAAc,CAAC,MAAM,GAAG,KAAK,GAAG,CAAC;AAAA,6BAChB,GAAG,IAAI,QAAQ,GAAG,CAAC;AAAA;AAAA;AAAA,MAEnD;AACA;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,KAAK,QAAQ,KAAK,aAAa,QAAQ,GAAG;AACpD,QAAI,SAAS,mBAAmB;AAC9B,cAAQ;AAAA,QACN,GAAG,GAAG,IAAI,oBAAe,CAAC,KAAK,GAAG,KAAK,GAAG,CAAC;AAAA,6BACb,GAAG,IAAI,QAAQ,GAAG,CAAC;AAAA;AAAA;AAAA,MAEnD;AACA;AAAA,IACF;AAAA,EACF;AAGA,UAAQ,IAAI,GAAG,IAAI,oDAAoD,CAAC;AACxE,MAAI,eAAe,KAAK,iBAAiB,GAAG;AAC1C,YAAQ,IAAI,GAAG,MAAM,uEAAkE,CAAC;AACxF,YAAQ,KAAK,CAAC;AAAA,EAChB,OAAO;AACL,YAAQ;AAAA,MACN,WAAW,GAAG,IAAI,GAAG,UAAU,WAAW,CAAC,KAAK,GAAG,OAAO,GAAG,YAAY,aAAa,CAAC;AAAA;AAAA,IACzF;AACA,QAAI,aAAa,EAAG,SAAQ,KAAK,CAAC;AAAA,EACpC;AACF,CAAC;AAEH,QAAQ,MAAM,QAAQ,IAAI;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ auditEnv: () => auditEnv
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/scanner.ts
38
+ var import_node_fs = __toESM(require("fs"), 1);
39
+ var import_glob = require("glob");
40
+ var import_parser = require("@babel/parser");
41
+ var import_traverse = __toESM(require("@babel/traverse"), 1);
42
+ var traverse = import_traverse.default.default || import_traverse.default;
43
+ function scanCodebase(rootDir = ".") {
44
+ const files = import_glob.glob.sync("**/*.{js,jsx,ts,tsx}", {
45
+ cwd: rootDir,
46
+ ignore: ["node_modules/**", "dist/**", ".next/**", "build/**"]
47
+ });
48
+ const discovered = [];
49
+ for (const file of files) {
50
+ try {
51
+ const code = import_node_fs.default.readFileSync(file, "utf-8");
52
+ const ast = (0, import_parser.parse)(code, {
53
+ sourceType: "module",
54
+ plugins: ["typescript", "jsx"]
55
+ });
56
+ traverse(ast, {
57
+ MemberExpression(path) {
58
+ const node = path.node;
59
+ if (node.object.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env" && node.property.type === "Identifier") {
60
+ discovered.push({
61
+ key: node.property.name,
62
+ filePath: file,
63
+ line: node.loc?.start.line || 0
64
+ });
65
+ }
66
+ if (node.object.type === "MemberExpression" && node.object.object?.type === "MetaProperty" && node.object.property?.name === "env" && node.property.type === "Identifier") {
67
+ discovered.push({
68
+ key: node.property.name,
69
+ filePath: file,
70
+ line: node.loc?.start.line || 0
71
+ });
72
+ }
73
+ }
74
+ });
75
+ } catch {
76
+ }
77
+ }
78
+ return discovered;
79
+ }
80
+
81
+ // src/envParser.ts
82
+ var import_node_fs2 = __toESM(require("fs"), 1);
83
+ var import_dotenv = __toESM(require("dotenv"), 1);
84
+ var SECRET_PATTERNS = [
85
+ /sk_live_[0-9a-zA-Z]{24,}/,
86
+ /AKIA[0-9A-Z]{16}/,
87
+ /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,
88
+ /-----BEGIN PRIVATE KEY-----/
89
+ ];
90
+ function parseEnvFile(envPath = ".env") {
91
+ const map = /* @__PURE__ */ new Map();
92
+ if (!import_node_fs2.default.existsSync(envPath)) return map;
93
+ const content = import_node_fs2.default.readFileSync(envPath, "utf-8");
94
+ const parsed = import_dotenv.default.parse(content);
95
+ for (const [key, value] of Object.entries(parsed)) {
96
+ const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));
97
+ map.set(key, { key, value, isPotentialSecret: isSecret });
98
+ }
99
+ return map;
100
+ }
101
+
102
+ // src/index.ts
103
+ function auditEnv(options = {}) {
104
+ const rootDir = options.rootDir || ".";
105
+ const envPath = options.envPath || ".env";
106
+ const usages = scanCodebase(rootDir);
107
+ const envMap = parseEnvFile(envPath);
108
+ const usedKeyNames = new Set(usages.map((u) => u.key));
109
+ const missingKeys = usages.filter((usage) => !envMap.has(usage.key));
110
+ const unusedKeys = Array.from(envMap.keys()).filter((key) => !usedKeyNames.has(key));
111
+ const leakedSecrets = Array.from(envMap.values()).filter((v) => v.isPotentialSecret);
112
+ return {
113
+ missingKeys,
114
+ unusedKeys,
115
+ leakedSecrets,
116
+ hasErrors: missingKeys.length > 0 || leakedSecrets.length > 0
117
+ };
118
+ }
119
+ // Annotate the CommonJS export names for ESM import in node:
120
+ 0 && (module.exports = {
121
+ auditEnv
122
+ });
123
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/scanner.ts","../src/envParser.ts"],"sourcesContent":[" // src/index.ts\n\n// 1. Mark type-only imports explicitly\nimport type { DiscoveredKey } from './scanner.js';\nimport type { EnvVariable } from './envParser.js';\n\n// 2. Value imports remain standard imports\nimport { scanCodebase } from './scanner.js';\nimport { parseEnvFile } from './envParser.js';\n\nexport interface AuditOptions {\n rootDir?: string;\n envPath?: string;\n}\n\nexport interface AuditReport {\n missingKeys: DiscoveredKey[];\n unusedKeys: string[];\n leakedSecrets: EnvVariable[];\n hasErrors: boolean;\n}\n\nexport function auditEnv(options: AuditOptions = {}): AuditReport {\n const rootDir = options.rootDir || '.';\n const envPath = options.envPath || '.env';\n\n const usages = scanCodebase(rootDir);\n const envMap = parseEnvFile(envPath);\n\n const usedKeyNames = new Set(usages.map((u) => u.key));\n\n const missingKeys = usages.filter((usage) => !envMap.has(usage.key));\n const unusedKeys = Array.from(envMap.keys()).filter((key) => !usedKeyNames.has(key));\n const leakedSecrets = Array.from(envMap.values()).filter((v) => v.isPotentialSecret);\n\n return {\n missingKeys,\n unusedKeys,\n leakedSecrets,\n hasErrors: missingKeys.length > 0 || leakedSecrets.length > 0,\n };\n}\n\nexport type { DiscoveredKey, EnvVariable };","import fs from 'node:fs';\nimport {glob}from 'glob';\nimport { parse } from '@babel/parser';\nimport traverseModule from '@babel/traverse';\n\nconst traverse = (traverseModule as any).default || traverseModule;\n\nexport interface DiscoveredKey {\n key: string;\n filePath: string;\n line: number;\n}\n\nexport function scanCodebase(rootDir = '.'): DiscoveredKey[] {\n const files = glob.sync('**/*.{js,jsx,ts,tsx}', {\n cwd: rootDir,\n ignore: ['node_modules/**', 'dist/**', '.next/**', 'build/**'],\n });\n\n const discovered: DiscoveredKey[] = [];\n\n for (const file of files) {\n try {\n const code = fs.readFileSync(file, 'utf-8');\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n });\n\n traverse(ast, {\n MemberExpression(path: any) {\n const node = path.node;\n // Detect process.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.name === 'process' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n // Detect import.meta.env.KEY\n if (\n node.object.type === 'MemberExpression' &&\n node.object.object?.type === 'MetaProperty' &&\n node.object.property?.name === 'env' &&\n node.property.type === 'Identifier'\n ) {\n discovered.push({\n key: node.property.name,\n filePath: file,\n line: node.loc?.start.line || 0,\n });\n }\n },\n });\n } catch {\n // Ignore unparseable or non-JS files\n }\n }\n\n return discovered;\n}","import fs from 'node:fs';\nimport dotenv from 'dotenv';\n\nexport interface EnvVariable {\n key: string;\n value: string;\n isPotentialSecret: boolean;\n}\n\n// High-risk pattern detector (Stripe, AWS, JWT, generic private keys)\nconst SECRET_PATTERNS = [\n /sk_live_[0-9a-zA-Z]{24,}/,\n /AKIA[0-9A-Z]{16}/,\n /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9/,\n /-----BEGIN PRIVATE KEY-----/,\n];\n\nexport function parseEnvFile(envPath = '.env'): Map<string, EnvVariable> {\n const map = new Map<string, EnvVariable>();\n if (!fs.existsSync(envPath)) return map;\n\n const content = fs.readFileSync(envPath, 'utf-8');\n const parsed = dotenv.parse(content);\n\n for (const [key, value] of Object.entries(parsed)) {\n const isSecret = SECRET_PATTERNS.some((pattern) => pattern.test(value));\n map.set(key, { key, value, isPotentialSecret: isSecret });\n }\n\n return map;\n}"],"mappings":"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","names":["traverseModule","fs","import_node_fs","fs","dotenv"]}
@@ -0,0 +1,25 @@
1
+ interface DiscoveredKey {
2
+ key: string;
3
+ filePath: string;
4
+ line: number;
5
+ }
6
+
7
+ interface EnvVariable {
8
+ key: string;
9
+ value: string;
10
+ isPotentialSecret: boolean;
11
+ }
12
+
13
+ interface AuditOptions {
14
+ rootDir?: string;
15
+ envPath?: string;
16
+ }
17
+ interface AuditReport {
18
+ missingKeys: DiscoveredKey[];
19
+ unusedKeys: string[];
20
+ leakedSecrets: EnvVariable[];
21
+ hasErrors: boolean;
22
+ }
23
+ declare function auditEnv(options?: AuditOptions): AuditReport;
24
+
25
+ export { type AuditOptions, type AuditReport, type DiscoveredKey, type EnvVariable, auditEnv };
@@ -0,0 +1,25 @@
1
+ interface DiscoveredKey {
2
+ key: string;
3
+ filePath: string;
4
+ line: number;
5
+ }
6
+
7
+ interface EnvVariable {
8
+ key: string;
9
+ value: string;
10
+ isPotentialSecret: boolean;
11
+ }
12
+
13
+ interface AuditOptions {
14
+ rootDir?: string;
15
+ envPath?: string;
16
+ }
17
+ interface AuditReport {
18
+ missingKeys: DiscoveredKey[];
19
+ unusedKeys: string[];
20
+ leakedSecrets: EnvVariable[];
21
+ hasErrors: boolean;
22
+ }
23
+ declare function auditEnv(options?: AuditOptions): AuditReport;
24
+
25
+ export { type AuditOptions, type AuditReport, type DiscoveredKey, type EnvVariable, auditEnv };
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import {
2
+ parseEnvFile,
3
+ scanCodebase
4
+ } from "./chunk-636KQXX3.js";
5
+
6
+ // src/index.ts
7
+ function auditEnv(options = {}) {
8
+ const rootDir = options.rootDir || ".";
9
+ const envPath = options.envPath || ".env";
10
+ const usages = scanCodebase(rootDir);
11
+ const envMap = parseEnvFile(envPath);
12
+ const usedKeyNames = new Set(usages.map((u) => u.key));
13
+ const missingKeys = usages.filter((usage) => !envMap.has(usage.key));
14
+ const unusedKeys = Array.from(envMap.keys()).filter((key) => !usedKeyNames.has(key));
15
+ const leakedSecrets = Array.from(envMap.values()).filter((v) => v.isPotentialSecret);
16
+ return {
17
+ missingKeys,
18
+ unusedKeys,
19
+ leakedSecrets,
20
+ hasErrors: missingKeys.length > 0 || leakedSecrets.length > 0
21
+ };
22
+ }
23
+ export {
24
+ auditEnv
25
+ };
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":[" // src/index.ts\n\n// 1. Mark type-only imports explicitly\nimport type { DiscoveredKey } from './scanner.js';\nimport type { EnvVariable } from './envParser.js';\n\n// 2. Value imports remain standard imports\nimport { scanCodebase } from './scanner.js';\nimport { parseEnvFile } from './envParser.js';\n\nexport interface AuditOptions {\n rootDir?: string;\n envPath?: string;\n}\n\nexport interface AuditReport {\n missingKeys: DiscoveredKey[];\n unusedKeys: string[];\n leakedSecrets: EnvVariable[];\n hasErrors: boolean;\n}\n\nexport function auditEnv(options: AuditOptions = {}): AuditReport {\n const rootDir = options.rootDir || '.';\n const envPath = options.envPath || '.env';\n\n const usages = scanCodebase(rootDir);\n const envMap = parseEnvFile(envPath);\n\n const usedKeyNames = new Set(usages.map((u) => u.key));\n\n const missingKeys = usages.filter((usage) => !envMap.has(usage.key));\n const unusedKeys = Array.from(envMap.keys()).filter((key) => !usedKeyNames.has(key));\n const leakedSecrets = Array.from(envMap.values()).filter((v) => v.isPotentialSecret);\n\n return {\n missingKeys,\n unusedKeys,\n leakedSecrets,\n hasErrors: missingKeys.length > 0 || leakedSecrets.length > 0,\n };\n}\n\nexport type { DiscoveredKey, EnvVariable };"],"mappings":";;;;;;AAsBO,SAAS,SAAS,UAAwB,CAAC,GAAgB;AAChE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,SAAS,aAAa,OAAO;AACnC,QAAM,SAAS,aAAa,OAAO;AAEnC,QAAM,eAAe,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAErD,QAAM,cAAc,OAAO,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,MAAM,GAAG,CAAC;AACnE,QAAM,aAAa,MAAM,KAAK,OAAO,KAAK,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,aAAa,IAAI,GAAG,CAAC;AACnF,QAAM,gBAAgB,MAAM,KAAK,OAAO,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,iBAAiB;AAEnF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,YAAY,SAAS,KAAK,cAAc,SAAS;AAAA,EAC9D;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "env-guardrail",
3
+ "version": "0.1.0",
4
+ "description": "Audit environment variables against codebase usage to catch missing keys, unused variables, and secret leaks.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "env-guardrail": "./dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "devDependencies": {
28
+ "@types/babel__traverse": "^7.20.5",
29
+ "@types/node": "^20.11.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.3.0"
32
+ },
33
+ "dependencies": {
34
+ "@babel/parser": "^7.23.0",
35
+ "@babel/traverse": "^7.23.0",
36
+ "commander": "^12.0.0",
37
+ "dotenv": "^16.4.0",
38
+ "glob": "^10.3.0",
39
+ "picocolors": "^1.0.0"
40
+ }
41
+ }
package/readme.md ADDED
@@ -0,0 +1,131 @@
1
+ # env-guardrail 🛡️
2
+
3
+ > Audit `.env` files against your codebase to catch missing variables, dead keys, and leaked production secrets before committing.
4
+
5
+ ## ⚡ Features
6
+
7
+ * **AST Codebase Scanning**: Static analysis parsing powered by Babel AST to accurately detect `process.env.VAR` and `import.meta.env.VAR` lookups across JS, JSX, TS, and TSX files.
8
+
9
+ * **Missing Variable Catching**: Flags environment variables referenced in code but missing from `.env` or `.env.example`.
10
+
11
+ * **Dead/Unused Key Cleanup**: Identifies orphaned variables declared in `.env` files that have zero references in your codebase.
12
+
13
+ * **Pre-Commit Secret Leak Prevention**: High-risk pattern scanner flags live production keys (Stripe, AWS, JWTs, RSA keys) inadvertently committed in local configuration files.
14
+
15
+ * **Zero-Config CLI & Programmatic API**: Use as an automated CLI check in pre-commit hooks/CI, or integrate directly into custom build tools with full TypeScript types.
16
+
17
+ ## 📦 Installation
18
+
19
+ ```
20
+ # Install globally for CLI usage
21
+ npm install -g env-guardrail
22
+
23
+ # Or install locally in your project
24
+ npm install --save-dev env-guardrail
25
+
26
+ ```
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ Run a check in any repository root directory containing your source code and `.env` files:
31
+
32
+ ```
33
+ npx env-guardrail check
34
+
35
+ ```
36
+
37
+ ### Example Terminal Output
38
+
39
+ ```
40
+ 🔍 Scanning codebase for environment variables...
41
+
42
+ ✖ MISSING KEY DATABASE_URL
43
+ Found in: src/db/client.ts:12
44
+ Action: Define this variable in .env
45
+
46
+ ⚠ UNUSED KEY OLD_REDIS_PORT
47
+ Defined in: .env
48
+ Action: No references found in codebase.
49
+
50
+ ✖ SECRET LEAK STRIPE_SECRET_KEY
51
+ Defined in: .env
52
+ Action: Value looks like an unmasked production secret key.
53
+
54
+ --------------------------------------------------
55
+ Result: 2 error(s), 1 warning(s)
56
+
57
+ ```
58
+
59
+ ## 🛠️ CLI Options
60
+
61
+ ```
62
+ env-guardrail check [options]
63
+
64
+ ```
65
+
66
+ | Option | Shorthand | Default | Description |
67
+ | ----- | ----- | ----- | ----- |
68
+ | `--env <path>` | `-e` | `.env` | Path to target environment file to audit |
69
+ | `--version` | `-v` | — | Display CLI version |
70
+ | `--help` | `-h` | — | Display help menu |
71
+
72
+ ## 💻 Programmatic TypeScript API
73
+
74
+ You can import `env-guardrail` into your Node.js scripts, Vite plugins, or custom test runners:
75
+
76
+ ```
77
+ import { auditEnv } from 'env-guardrail';
78
+
79
+ const report = auditEnv({
80
+ rootDir: './src',
81
+ envPath: '.env.local',
82
+ });
83
+
84
+ if (report.hasErrors) {
85
+ console.error('Missing variables:', report.missingKeys);
86
+ console.error('Potential secret leaks:', report.leakedSecrets);
87
+ process.exit(1);
88
+ }
89
+
90
+ ```
91
+
92
+ ## 🔄 CI/CD & Git Hook Integration
93
+
94
+ ### 1. Husky Pre-Commit Hook
95
+
96
+ Prevent committing broken configuration or leaked secrets locally:
97
+
98
+ ```
99
+ npx husky add .husky/pre-commit "npx env-guardrail check"
100
+
101
+ ```
102
+
103
+ ### 2. GitHub Actions
104
+
105
+ Fail pull requests early if required environment variables are missing from `.env.example`:
106
+
107
+ ```
108
+ name: Audit Environment Configuration
109
+
110
+ on:
111
+ push:
112
+ branches: [main]
113
+ pull_request:
114
+ branches: [main]
115
+
116
+ jobs:
117
+ env-audit:
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+ - uses: actions/setup-node@v4
122
+ with:
123
+ node-version: 20
124
+ - run: npm ci
125
+ - run: npx env-guardrail check --env .env.example
126
+
127
+ ```
128
+
129
+ ## 📄 License
130
+
131
+ MIT © Kaustubh Kashyap