env-detector 1.1.0 ā 1.3.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/README.md +24 -10
- package/bin/env-scan.js +94 -38
- package/package.json +1 -1
- package/src/scan.js +36 -5
package/README.md
CHANGED
|
@@ -19,16 +19,30 @@ env-detector
|
|
|
19
19
|
|
|
20
20
|
## Commands
|
|
21
21
|
|
|
22
|
-
| Command | Description |
|
|
23
|
-
|
|
24
|
-
| `env-detector` | Generate `.env` |
|
|
25
|
-
| `env-detector --
|
|
26
|
-
| `env-detector --
|
|
27
|
-
| `env-detector --
|
|
28
|
-
| `env-detector --
|
|
29
|
-
| `env-detector --
|
|
30
|
-
| `env-detector --
|
|
31
|
-
| `env-detector --
|
|
22
|
+
| Command | Shorthand | Description |
|
|
23
|
+
|---------|-----------|-------------|
|
|
24
|
+
| `env-detector` | | Generate `.env` |
|
|
25
|
+
| `env-detector --ask` | `-a` | Interactive mode to fill missing or empty values |
|
|
26
|
+
| `env-detector --compare` | `-c` | Show detailed comparison of used, missing, empty, and unused variables |
|
|
27
|
+
| `env-detector --check` | `-k` | Exit with error if variables are missing or empty |
|
|
28
|
+
| `env-detector --fix` | `-f` | **Interactive** cleanup of unused variables |
|
|
29
|
+
| `env-detector --security` | `-s` | Scan for hardcoded secrets in source files and `.env` |
|
|
30
|
+
| `env-detector --strict` | `-t` | Strict mode (CI) with detailed failure reporting |
|
|
31
|
+
| `env-detector --help` | `-h` | Show help message |
|
|
32
|
+
| `env-detector --version` | `-v` | Show version |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
### š Interactive Fix
|
|
39
|
+
When running with `--fix` or `-f`, the tool doesn't just delete variables. It lists every unused key it finds and asks for your confirmation (`y/n`) before removing it.
|
|
40
|
+
|
|
41
|
+
### š Detailed Strict Mode
|
|
42
|
+
Ideal for CI/CD pipelines. If `strict` mode fails, it will provide a categorized list of exactly what triggered the failure:
|
|
43
|
+
- **Missing**: Variables used in code but not in `.env`.
|
|
44
|
+
- **Empty**: Variables in `.env` without values.
|
|
45
|
+
- **Unused**: Variables in `.env` not found in code.
|
|
32
46
|
|
|
33
47
|
---
|
|
34
48
|
|
package/bin/env-scan.js
CHANGED
|
@@ -10,32 +10,47 @@ const envPath = path.join(cwd, ".env");
|
|
|
10
10
|
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
|
|
13
|
-
const askMode = args.includes("--ask");
|
|
14
|
-
const compareMode = args.includes("--compare");
|
|
15
|
-
const checkMode = args.includes("--check");
|
|
16
|
-
const fixMode = args.includes("--fix");
|
|
17
|
-
const securityMode = args.includes("--security");
|
|
18
|
-
const strictMode = args.includes("--strict");
|
|
19
|
-
const versionMode = args.includes("--version") || args.includes("
|
|
13
|
+
const askMode = args.includes("--ask") || args.includes("-a");
|
|
14
|
+
const compareMode = args.includes("--compare") || args.includes("-c");
|
|
15
|
+
const checkMode = args.includes("--check") || args.includes("-k");
|
|
16
|
+
const fixMode = args.includes("--fix") || args.includes("-f");
|
|
17
|
+
const securityMode = args.includes("--security") || args.includes("-s");
|
|
18
|
+
const strictMode = args.includes("--strict") || args.includes("-t");
|
|
19
|
+
const versionMode = args.includes("--version") || args.includes("-v")
|
|
20
20
|
const helpMode = args.includes("--help") || args.includes("-h");
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const validFlags = [
|
|
23
|
+
"--ask", "-a",
|
|
24
|
+
"--compare", "-c",
|
|
25
|
+
"--check", "-k",
|
|
26
|
+
"--fix", "-f",
|
|
27
|
+
"--security", "-s",
|
|
28
|
+
"--strict", "-t",
|
|
29
|
+
"--version", "-v",
|
|
30
|
+
"--help", "-h"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const unknownFlag = args.find(arg => arg.startsWith("-") && !validFlags.includes(arg));
|
|
34
|
+
|
|
35
|
+
if (unknownFlag && !helpMode) {
|
|
36
|
+
console.log(`\nError: Unknown flag "${unknownFlag}"`);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
if (helpMode) {
|
|
39
|
+
if (helpMode || (unknownFlag && !helpMode)) {
|
|
25
40
|
console.log(`
|
|
26
41
|
Usage: env-detector [options]
|
|
27
42
|
|
|
28
43
|
Options:
|
|
29
|
-
--ask Interactive mode to fill missing or empty values
|
|
30
|
-
--compare Show detailed comparison of used, missing, empty, and unused variables
|
|
31
|
-
--check Exit with error if variables are missing or empty
|
|
32
|
-
--fix Remove unused variables from .env
|
|
33
|
-
--security Scan for hardcoded secrets in source files and .env
|
|
34
|
-
--strict Fail if any issues (missing, empty, or unused) are found
|
|
35
|
-
--version Show version information
|
|
36
|
-
--help
|
|
44
|
+
-a, --ask Interactive mode to fill missing or empty values
|
|
45
|
+
-c, --compare Show detailed comparison of used, missing, empty, and unused variables
|
|
46
|
+
-k, --check Exit with error if variables are missing or empty
|
|
47
|
+
-f, --fix Remove unused variables from .env
|
|
48
|
+
-s, --security Scan for hardcoded secrets in source files and .env
|
|
49
|
+
-t, --strict Fail if any issues (missing, empty, or unused) are found
|
|
50
|
+
-v, --version Show version information
|
|
51
|
+
-h, --help Show this help message
|
|
37
52
|
`);
|
|
38
|
-
process.exit(0);
|
|
53
|
+
process.exit(unknownFlag ? 1 : 0);
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
if (versionMode) {
|
|
@@ -44,6 +59,28 @@ if (versionMode) {
|
|
|
44
59
|
process.exit(0);
|
|
45
60
|
}
|
|
46
61
|
|
|
62
|
+
let result = scanProject(cwd);
|
|
63
|
+
|
|
64
|
+
// interactive fix
|
|
65
|
+
const varsToDelete = new Set();
|
|
66
|
+
|
|
67
|
+
if (fixMode && result.unused.length) {
|
|
68
|
+
console.log("\nReview unused variables:");
|
|
69
|
+
result.unused.forEach(key => {
|
|
70
|
+
if (readline.keyInYN(`Delete unused variable "${key}"?`)) {
|
|
71
|
+
varsToDelete.add(key);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
console.log("");
|
|
75
|
+
|
|
76
|
+
// update result to reflect choices
|
|
77
|
+
const originalUnused = [...result.unused];
|
|
78
|
+
result.unused = originalUnused.filter(k => varsToDelete.has(k));
|
|
79
|
+
// if we chose NOT to delete it, it's effectively "used" for this run's purposes
|
|
80
|
+
const kept = originalUnused.filter(k => !varsToDelete.has(k));
|
|
81
|
+
result.used.push(...kept);
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
// security
|
|
48
85
|
if (securityMode) {
|
|
49
86
|
const issues = scanSecurity(cwd);
|
|
@@ -53,9 +90,11 @@ if (securityMode) {
|
|
|
53
90
|
process.exit(0);
|
|
54
91
|
}
|
|
55
92
|
|
|
56
|
-
console.log("\nSecurity issues:\n");
|
|
57
|
-
issues.forEach(i =>
|
|
58
|
-
|
|
93
|
+
console.log("\nSecurity issues found:\n");
|
|
94
|
+
issues.forEach(i => {
|
|
95
|
+
console.log(` - ${i.file}:${i.line}`);
|
|
96
|
+
console.log(` ${i.snippet}\n`);
|
|
97
|
+
});
|
|
59
98
|
|
|
60
99
|
process.exit(0);
|
|
61
100
|
}
|
|
@@ -100,6 +139,37 @@ if (checkMode) {
|
|
|
100
139
|
}
|
|
101
140
|
|
|
102
141
|
|
|
142
|
+
if (strictMode) {
|
|
143
|
+
let failed = false;
|
|
144
|
+
|
|
145
|
+
if (result.missing.length) {
|
|
146
|
+
failed = true;
|
|
147
|
+
console.log("\nMissing variables:");
|
|
148
|
+
result.missing.forEach(v => console.log(` - ${v}`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.empty.length) {
|
|
152
|
+
failed = true;
|
|
153
|
+
console.log("\nEmpty variables:");
|
|
154
|
+
result.empty.forEach(v => console.log(` - ${v}`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (result.unused.length) {
|
|
158
|
+
failed = true;
|
|
159
|
+
console.log("\nUnused variables:");
|
|
160
|
+
result.unused.forEach(v => console.log(` - ${v}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (failed) {
|
|
164
|
+
console.log("\nā strict mode failed\n");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log("ā strict mode passed\n");
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
103
173
|
// create env
|
|
104
174
|
if (!fs.existsSync(envPath)) {
|
|
105
175
|
fs.writeFileSync(envPath, "");
|
|
@@ -157,6 +227,8 @@ let content = fs.existsSync(envPath)
|
|
|
157
227
|
const envMap = {};
|
|
158
228
|
|
|
159
229
|
content.split("\n").forEach(line => {
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
160
232
|
const [k, ...rest] = line.split("=");
|
|
161
233
|
if (!k) return;
|
|
162
234
|
envMap[k.trim()] = rest.join("=");
|
|
@@ -195,7 +267,7 @@ result.missing.forEach(key => {
|
|
|
195
267
|
|
|
196
268
|
// fix unused
|
|
197
269
|
if (fixMode) {
|
|
198
|
-
|
|
270
|
+
varsToDelete.forEach(key => delete envMap[key]);
|
|
199
271
|
}
|
|
200
272
|
|
|
201
273
|
|
|
@@ -206,20 +278,4 @@ const newContent = Object.entries(envMap)
|
|
|
206
278
|
fs.writeFileSync(envPath, newContent + "\n");
|
|
207
279
|
|
|
208
280
|
|
|
209
|
-
// strict
|
|
210
|
-
if (strictMode) {
|
|
211
|
-
if (
|
|
212
|
-
result.missing.length ||
|
|
213
|
-
result.empty.length ||
|
|
214
|
-
result.unused.length
|
|
215
|
-
) {
|
|
216
|
-
console.log("ā strict mode failed\n");
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
console.log("ā strict mode passed\n");
|
|
221
|
-
process.exit(0);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
281
|
console.log("ā env scan complete\n");
|
package/package.json
CHANGED
package/src/scan.js
CHANGED
|
@@ -215,14 +215,20 @@ function scanSecurity(rootDir) {
|
|
|
215
215
|
const issues = [];
|
|
216
216
|
|
|
217
217
|
const regex =
|
|
218
|
-
/(password|secret|token|apikey|key)\s*[:=]\s*['"][^'"]
|
|
218
|
+
/(?<![a-zA-Z0-9])(password|secret|token|apikey|key)\b\s*[:=]\s*['"](?!(string|varchar|text|number|boolean|uuid|auto_increment|primary_key|null|undefined|true|false|name|id|type|label|department|category|field|header|consumption|duration)['"])[^'"]{8,}['"]/gi;
|
|
219
219
|
|
|
220
220
|
function scan(dir) {
|
|
221
221
|
const files = fs.readdirSync(dir);
|
|
222
222
|
|
|
223
223
|
for (const file of files) {
|
|
224
224
|
|
|
225
|
-
if (
|
|
225
|
+
if (
|
|
226
|
+
file === "node_modules" ||
|
|
227
|
+
file === ".git" ||
|
|
228
|
+
file === "dist" ||
|
|
229
|
+
file === "build" ||
|
|
230
|
+
file.startsWith(".")
|
|
231
|
+
) continue;
|
|
226
232
|
|
|
227
233
|
const full = path.join(dir, file);
|
|
228
234
|
const stat = fs.statSync(full);
|
|
@@ -232,9 +238,34 @@ function scanSecurity(rootDir) {
|
|
|
232
238
|
} else if (/\.(js|ts|env)$/.test(file)) {
|
|
233
239
|
const content = fs.readFileSync(full, "utf8");
|
|
234
240
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
const lines = content.split("\n");
|
|
242
|
+
lines.forEach((line, index) => {
|
|
243
|
+
regex.lastIndex = 0;
|
|
244
|
+
const match = regex.exec(line);
|
|
245
|
+
if (match) {
|
|
246
|
+
const matchedWord = match[1].toLowerCase();
|
|
247
|
+
const fullMatch = match[0];
|
|
248
|
+
const valMatch = fullMatch.match(/['"](.*?)['"]/);
|
|
249
|
+
if (!valMatch) return;
|
|
250
|
+
|
|
251
|
+
const value = valMatch[1];
|
|
252
|
+
|
|
253
|
+
// Ignore paths/URLs
|
|
254
|
+
if (value.startsWith("/")) return;
|
|
255
|
+
|
|
256
|
+
// Stricter check for generic "key" property
|
|
257
|
+
if (matchedWord === "key") {
|
|
258
|
+
const hasComplexity = /[0-9!@#$%^&*()+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
|
|
259
|
+
if (!hasComplexity) return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
issues.push({
|
|
263
|
+
file: full,
|
|
264
|
+
line: index + 1,
|
|
265
|
+
snippet: line.trim()
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
238
269
|
}
|
|
239
270
|
}
|
|
240
271
|
}
|