env-detector 1.0.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/LICENSE +2 -0
- package/README.md +25 -0
- package/bin/env-scan.js +204 -0
- package/package.json +21 -0
- package/src/scan.js +249 -0
- package/src/writer.js +24 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# env-scan
|
|
2
|
+
|
|
3
|
+
Automatically generate .env file by scanning process.env usage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
npx env-scan
|
|
8
|
+
|
|
9
|
+
OR
|
|
10
|
+
|
|
11
|
+
npm install -g env-scan
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
env-scan
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
process.env.DB_HOST
|
|
20
|
+
process.env.PORT
|
|
21
|
+
|
|
22
|
+
Generated:
|
|
23
|
+
|
|
24
|
+
DB_HOST=
|
|
25
|
+
PORT=
|
package/bin/env-scan.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline-sync");
|
|
6
|
+
const { scanProject, scanSecurity } = require("../src/scan");
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const envPath = path.join(cwd, ".env");
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
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
|
+
|
|
20
|
+
let result = scanProject(cwd);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// security
|
|
24
|
+
if (securityMode) {
|
|
25
|
+
const issues = scanSecurity(cwd);
|
|
26
|
+
|
|
27
|
+
if (!issues.length) {
|
|
28
|
+
console.log("✔ No security issues\n");
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("\nSecurity issues:\n");
|
|
33
|
+
issues.forEach(i => console.log(" -", i.file));
|
|
34
|
+
console.log("");
|
|
35
|
+
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// compare
|
|
41
|
+
if (compareMode) {
|
|
42
|
+
|
|
43
|
+
console.log("\nUsed:");
|
|
44
|
+
result.used.forEach(v => console.log(" -", v));
|
|
45
|
+
|
|
46
|
+
console.log("\nMissing:");
|
|
47
|
+
result.missing.forEach(v => console.log(" -", v));
|
|
48
|
+
|
|
49
|
+
console.log("\nEmpty:");
|
|
50
|
+
result.empty.forEach(v => console.log(" -", v));
|
|
51
|
+
|
|
52
|
+
console.log("\nUnused:");
|
|
53
|
+
result.unused.forEach(v => console.log(" -", v));
|
|
54
|
+
|
|
55
|
+
console.log("");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// check
|
|
61
|
+
if (checkMode) {
|
|
62
|
+
|
|
63
|
+
if (result.missing.length || result.empty.length) {
|
|
64
|
+
|
|
65
|
+
console.log("\nENV check failed");
|
|
66
|
+
|
|
67
|
+
result.missing.forEach(v => console.log("Missing:", v));
|
|
68
|
+
result.empty.forEach(v => console.log("Empty:", v));
|
|
69
|
+
|
|
70
|
+
console.log("");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("✔ ENV check passed\n");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// create env
|
|
80
|
+
if (!fs.existsSync(envPath)) {
|
|
81
|
+
fs.writeFileSync(envPath, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// grouped generation
|
|
86
|
+
if (Object.keys(result.grouped).length) {
|
|
87
|
+
|
|
88
|
+
let output = "";
|
|
89
|
+
|
|
90
|
+
// collect grouped keys
|
|
91
|
+
const groupedKeys = new Set();
|
|
92
|
+
|
|
93
|
+
Object.values(result.grouped).forEach(keys => {
|
|
94
|
+
keys.forEach(k => groupedKeys.add(k));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// GLOBAL VARIABLES
|
|
98
|
+
const globalKeys = result.used.filter(k => !groupedKeys.has(k));
|
|
99
|
+
|
|
100
|
+
if (globalKeys.length) {
|
|
101
|
+
output += "# global\n";
|
|
102
|
+
|
|
103
|
+
globalKeys.forEach(key => {
|
|
104
|
+
const value = result.defaults?.[key] ?? "";
|
|
105
|
+
output += `${key}=${value}\n`;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
output += "\n";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// GROUPED ENVIRONMENTS
|
|
112
|
+
Object.entries(result.grouped).forEach(([env, keys]) => {
|
|
113
|
+
|
|
114
|
+
output += `# ${env}\n`;
|
|
115
|
+
|
|
116
|
+
keys.forEach(key => {
|
|
117
|
+
const value = result.defaults?.[key] ?? "";
|
|
118
|
+
output += `${key}=${value}\n`;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
output += "\n";
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(envPath, output.trim() + "\n");
|
|
125
|
+
|
|
126
|
+
console.log("✔ Generated grouped env file\n");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// parse env to map
|
|
132
|
+
let content = fs.existsSync(envPath)
|
|
133
|
+
? fs.readFileSync(envPath, "utf8")
|
|
134
|
+
: "";
|
|
135
|
+
|
|
136
|
+
const envMap = {};
|
|
137
|
+
|
|
138
|
+
content.split("\n").forEach(line => {
|
|
139
|
+
const [k, ...rest] = line.split("=");
|
|
140
|
+
if (!k) return;
|
|
141
|
+
envMap[k.trim()] = rest.join("=");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
// ask mode
|
|
146
|
+
const askList = [...new Set([...result.missing, ...result.empty])];
|
|
147
|
+
|
|
148
|
+
if (askMode && askList.length) {
|
|
149
|
+
|
|
150
|
+
askList.forEach(key => {
|
|
151
|
+
const value = readline.question(`${key} = `);
|
|
152
|
+
envMap[key] = value;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const newContent = Object.entries(envMap)
|
|
156
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
157
|
+
.join("\n");
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(envPath, newContent + "\n");
|
|
160
|
+
|
|
161
|
+
console.log("✔ .env updated\n");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// add missing
|
|
167
|
+
result.missing.forEach(key => {
|
|
168
|
+
const value = result.defaults?.[key] ?? "";
|
|
169
|
+
if (!envMap[key]) {
|
|
170
|
+
envMap[key] = value;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// fix unused
|
|
176
|
+
if (fixMode) {
|
|
177
|
+
result.unused.forEach(key => delete envMap[key]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
const newContent = Object.entries(envMap)
|
|
182
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
183
|
+
.join("\n");
|
|
184
|
+
|
|
185
|
+
fs.writeFileSync(envPath, newContent + "\n");
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
// strict
|
|
189
|
+
if (strictMode) {
|
|
190
|
+
if (
|
|
191
|
+
result.missing.length ||
|
|
192
|
+
result.empty.length ||
|
|
193
|
+
result.unused.length
|
|
194
|
+
) {
|
|
195
|
+
console.log("✖ strict mode failed\n");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log("✔ strict mode passed\n");
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
console.log("✔ env scan complete\n");
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "env-detector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Smart .env generator and auditor",
|
|
5
|
+
"main": "src/scan.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"env-detector": "./bin/env-scan.js",
|
|
8
|
+
"env-scan": "./bin/env-scan.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["env", "dotenv", "environment", "env-scan", "env-detector"],
|
|
14
|
+
"author": "Jenil Gajjar",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@babel/parser": "^7.29.2",
|
|
18
|
+
"@babel/traverse": "^7.29.0",
|
|
19
|
+
"readline-sync": "^1.4.10"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/scan.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const parser = require("@babel/parser");
|
|
4
|
+
const traverse = require("@babel/traverse").default;
|
|
5
|
+
|
|
6
|
+
function scanProject(rootDir) {
|
|
7
|
+
const usedEnvVars = new Set();
|
|
8
|
+
const defaultValues = new Map();
|
|
9
|
+
const groupedEnv = {};
|
|
10
|
+
|
|
11
|
+
function scanDir(dir) {
|
|
12
|
+
const files = fs.readdirSync(dir);
|
|
13
|
+
|
|
14
|
+
for (const file of files) {
|
|
15
|
+
if (
|
|
16
|
+
file === "node_modules" ||
|
|
17
|
+
file === ".git" ||
|
|
18
|
+
file === "dist" ||
|
|
19
|
+
file === "build" ||
|
|
20
|
+
file.startsWith(".")
|
|
21
|
+
) continue;
|
|
22
|
+
|
|
23
|
+
const fullPath = path.join(dir, file);
|
|
24
|
+
const stat = fs.statSync(fullPath);
|
|
25
|
+
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
scanDir(fullPath);
|
|
28
|
+
} else if (/\.(js|ts|jsx|tsx)$/.test(file)) {
|
|
29
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const ast = parser.parse(content, {
|
|
33
|
+
sourceType: "module",
|
|
34
|
+
plugins: ["typescript", "jsx"],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
traverse(ast, {
|
|
38
|
+
|
|
39
|
+
// detect grouped config (development/staging/etc)
|
|
40
|
+
ObjectProperty(path) {
|
|
41
|
+
|
|
42
|
+
const envName = path.node.key.name;
|
|
43
|
+
|
|
44
|
+
if (!path.node.value || path.node.value.type !== "ObjectExpression")
|
|
45
|
+
return;
|
|
46
|
+
|
|
47
|
+
const vars = new Set();
|
|
48
|
+
|
|
49
|
+
path.node.value.properties.forEach(prop => {
|
|
50
|
+
|
|
51
|
+
if (!prop.value) return;
|
|
52
|
+
|
|
53
|
+
// process.env.KEY
|
|
54
|
+
if (
|
|
55
|
+
prop.value.type === "MemberExpression" &&
|
|
56
|
+
prop.value.object?.object?.name === "process" &&
|
|
57
|
+
prop.value.object?.property?.name === "env"
|
|
58
|
+
) {
|
|
59
|
+
const key =
|
|
60
|
+
prop.value.property.name ||
|
|
61
|
+
prop.value.property.value;
|
|
62
|
+
|
|
63
|
+
vars.add(key);
|
|
64
|
+
usedEnvVars.add(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// process.env.KEY || value
|
|
68
|
+
if (prop.value.type === "LogicalExpression") {
|
|
69
|
+
|
|
70
|
+
const left = prop.value.left;
|
|
71
|
+
const right = prop.value.right;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
left.type === "MemberExpression" &&
|
|
75
|
+
left.object?.object?.name === "process"
|
|
76
|
+
) {
|
|
77
|
+
const key =
|
|
78
|
+
left.property.name || left.property.value;
|
|
79
|
+
|
|
80
|
+
vars.add(key);
|
|
81
|
+
usedEnvVars.add(key);
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
right.type === "StringLiteral" ||
|
|
85
|
+
right.type === "NumericLiteral" ||
|
|
86
|
+
right.type === "BooleanLiteral"
|
|
87
|
+
) {
|
|
88
|
+
defaultValues.set(key, right.value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (vars.size) {
|
|
96
|
+
groupedEnv[envName] = Array.from(vars);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// standalone process.env.KEY
|
|
101
|
+
MemberExpression(path) {
|
|
102
|
+
const node = path.node;
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
node.object &&
|
|
106
|
+
node.object.type === "MemberExpression" &&
|
|
107
|
+
node.object.object?.name === "process" &&
|
|
108
|
+
node.object.property?.name === "env"
|
|
109
|
+
) {
|
|
110
|
+
const key =
|
|
111
|
+
node.property.name || node.property.value;
|
|
112
|
+
|
|
113
|
+
usedEnvVars.add(key);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// destructuring
|
|
118
|
+
VariableDeclarator(path) {
|
|
119
|
+
if (
|
|
120
|
+
path.node.init &&
|
|
121
|
+
path.node.init.type === "MemberExpression" &&
|
|
122
|
+
path.node.init.object.name === "process" &&
|
|
123
|
+
path.node.init.property.name === "env"
|
|
124
|
+
) {
|
|
125
|
+
if (path.node.id.type === "ObjectPattern") {
|
|
126
|
+
path.node.id.properties.forEach(prop => {
|
|
127
|
+
if (prop.key?.name) {
|
|
128
|
+
usedEnvVars.add(prop.key.name);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// fallback detection
|
|
136
|
+
LogicalExpression(path) {
|
|
137
|
+
const left = path.node.left;
|
|
138
|
+
const right = path.node.right;
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
left.type === "MemberExpression" &&
|
|
142
|
+
left.object?.object?.name === "process" &&
|
|
143
|
+
left.object?.property?.name === "env"
|
|
144
|
+
) {
|
|
145
|
+
const key =
|
|
146
|
+
left.property.name || left.property.value;
|
|
147
|
+
|
|
148
|
+
usedEnvVars.add(key);
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
right.type === "StringLiteral" ||
|
|
152
|
+
right.type === "NumericLiteral" ||
|
|
153
|
+
right.type === "BooleanLiteral"
|
|
154
|
+
) {
|
|
155
|
+
defaultValues.set(key, right.value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
} catch (err) {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
scanDir(rootDir);
|
|
168
|
+
|
|
169
|
+
// read existing env
|
|
170
|
+
const envPath = path.join(rootDir, ".env");
|
|
171
|
+
const envFileVars = new Set();
|
|
172
|
+
const emptyVars = new Set();
|
|
173
|
+
|
|
174
|
+
if (fs.existsSync(envPath)) {
|
|
175
|
+
const envContent = fs.readFileSync(envPath, "utf8");
|
|
176
|
+
|
|
177
|
+
envContent.split("\n").forEach(line => {
|
|
178
|
+
const trimmed = line.trim();
|
|
179
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
180
|
+
|
|
181
|
+
const [key, value] = trimmed.split("=");
|
|
182
|
+
|
|
183
|
+
if (key) {
|
|
184
|
+
envFileVars.add(key.trim());
|
|
185
|
+
|
|
186
|
+
if (!value || value.trim() === "") {
|
|
187
|
+
emptyVars.add(key.trim());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const missing = [...usedEnvVars].filter(
|
|
194
|
+
key => !envFileVars.has(key)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const unused = [...envFileVars].filter(
|
|
198
|
+
key => !usedEnvVars.has(key)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
used: Array.from(usedEnvVars),
|
|
203
|
+
missing,
|
|
204
|
+
unused,
|
|
205
|
+
empty: Array.from(emptyVars),
|
|
206
|
+
defaults: Object.fromEntries(defaultValues),
|
|
207
|
+
grouped: groupedEnv
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
// security scan
|
|
213
|
+
function scanSecurity(rootDir) {
|
|
214
|
+
|
|
215
|
+
const issues = [];
|
|
216
|
+
|
|
217
|
+
const regex =
|
|
218
|
+
/(password|secret|token|apikey|key)\s*[:=]\s*['"][^'"]+['"]/gi;
|
|
219
|
+
|
|
220
|
+
function scan(dir) {
|
|
221
|
+
const files = fs.readdirSync(dir);
|
|
222
|
+
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
|
|
225
|
+
if (file === "node_modules" || file === ".git") continue;
|
|
226
|
+
|
|
227
|
+
const full = path.join(dir, file);
|
|
228
|
+
const stat = fs.statSync(full);
|
|
229
|
+
|
|
230
|
+
if (stat.isDirectory()) {
|
|
231
|
+
scan(full);
|
|
232
|
+
} else if (/\.(js|ts|env)$/.test(file)) {
|
|
233
|
+
const content = fs.readFileSync(full, "utf8");
|
|
234
|
+
|
|
235
|
+
if (regex.test(content)) {
|
|
236
|
+
issues.push({ file: full });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
scan(rootDir);
|
|
243
|
+
return issues;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
scanProject,
|
|
248
|
+
scanSecurity
|
|
249
|
+
};
|
package/src/writer.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function updateEnv(vars) {
|
|
5
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
6
|
+
|
|
7
|
+
let existing = "";
|
|
8
|
+
|
|
9
|
+
if (fs.existsSync(envPath)) {
|
|
10
|
+
existing = fs.readFileSync(envPath, "utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let output = existing;
|
|
14
|
+
|
|
15
|
+
vars.forEach(key => {
|
|
16
|
+
if (!existing.includes(key + "=")) {
|
|
17
|
+
output += `\n${key}=`;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
fs.writeFileSync(envPath, output);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { updateEnv };
|