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.
- package/dist/chunk-636KQXX3.js +70 -0
- package/dist/chunk-636KQXX3.js.map +1 -0
- package/dist/cli.cjs +151 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +123 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/readme.md +131 -0
|
@@ -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
|
package/dist/cli.cjs.map
ADDED
|
@@ -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
|
package/dist/cli.js.map
ADDED
|
@@ -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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|