@danielszlaski/envguard 0.1.3 → 0.1.5
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/.envguardrc.example.json +1 -0
- package/.envguardrc.json +16 -0
- package/.github/FUNDING.yml +15 -0
- package/LICENSE +1 -1
- package/README.md +285 -10
- package/dist/analyzer/envAnalyzer.d.ts +8 -2
- package/dist/analyzer/envAnalyzer.d.ts.map +1 -1
- package/dist/analyzer/envAnalyzer.js +22 -8
- package/dist/analyzer/envAnalyzer.js.map +1 -1
- package/dist/cli.js +58 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +32 -7
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/install-hook.d.ts +18 -0
- package/dist/commands/install-hook.d.ts.map +1 -0
- package/dist/commands/install-hook.js +148 -0
- package/dist/commands/install-hook.js.map +1 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +150 -41
- package/dist/commands/scan.js.map +1 -1
- package/dist/config/configLoader.d.ts +6 -0
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +1 -0
- package/dist/config/configLoader.js.map +1 -1
- package/dist/scanner/codeScanner.d.ts +5 -2
- package/dist/scanner/codeScanner.d.ts.map +1 -1
- package/dist/scanner/codeScanner.js +72 -25
- package/dist/scanner/codeScanner.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/analyzer/envAnalyzer.ts +27 -10
- package/src/cli.ts +62 -3
- package/src/commands/fix.ts +40 -9
- package/src/commands/install-hook.ts +128 -0
- package/src/commands/scan.ts +168 -47
- package/src/config/configLoader.ts +8 -0
- package/src/scanner/codeScanner.ts +97 -28
- package/src/types.ts +3 -1
- package/test-project/src/lambda2/handler2.js +1 -1
- package/test-project/.envguardrc.json +0 -7
|
@@ -3,6 +3,8 @@ import * as path from 'path';
|
|
|
3
3
|
import { glob } from 'glob';
|
|
4
4
|
import { ServerlessParser } from '../parser/serverlessParser';
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
|
|
6
8
|
export class CodeScanner {
|
|
7
9
|
private rootDir: string;
|
|
8
10
|
private excludePatterns: string[];
|
|
@@ -14,24 +16,32 @@ export class CodeScanner {
|
|
|
14
16
|
this.serverlessParser = new ServerlessParser();
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
async scan(): Promise<Map<string, string[]>> {
|
|
18
|
-
const envVars = new Map<string, string[]>();
|
|
19
|
+
async scan(): Promise<Map<string, { locations: string[], hasFallback: boolean }>> {
|
|
20
|
+
const envVars = new Map<string, { locations: string[], hasFallback: boolean }>();
|
|
19
21
|
|
|
20
22
|
// Scan for JavaScript/TypeScript files
|
|
23
|
+
// Handle both simple names (e.g., 'node_modules') and glob patterns (e.g., '**/tmp/**')
|
|
24
|
+
const ignorePatterns = this.excludePatterns.map(p =>
|
|
25
|
+
p.includes('*') ? p : `**/${p}/**`
|
|
26
|
+
);
|
|
27
|
+
|
|
21
28
|
const files = await glob('**/*.{js,ts,jsx,tsx,mjs,cjs}', {
|
|
22
29
|
cwd: this.rootDir,
|
|
23
|
-
ignore:
|
|
30
|
+
ignore: ignorePatterns,
|
|
24
31
|
absolute: true,
|
|
25
32
|
});
|
|
26
33
|
|
|
27
34
|
for (const file of files) {
|
|
28
35
|
const vars = await this.scanFile(file);
|
|
29
|
-
for (const varName of vars) {
|
|
36
|
+
for (const [varName, hasFallback] of vars.entries()) {
|
|
30
37
|
const relativePath = path.relative(this.rootDir, file);
|
|
31
38
|
if (!envVars.has(varName)) {
|
|
32
|
-
envVars.set(varName, []);
|
|
39
|
+
envVars.set(varName, { locations: [], hasFallback: false });
|
|
33
40
|
}
|
|
34
|
-
envVars.get(varName)
|
|
41
|
+
const entry = envVars.get(varName)!;
|
|
42
|
+
entry.locations.push(relativePath);
|
|
43
|
+
// If ANY usage has a fallback, mark it as having a fallback
|
|
44
|
+
entry.hasFallback = entry.hasFallback || hasFallback;
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -42,48 +52,95 @@ export class CodeScanner {
|
|
|
42
52
|
for (const [varName, entry] of vars.entries()) {
|
|
43
53
|
const relativePath = path.relative(this.rootDir, file);
|
|
44
54
|
if (!envVars.has(varName)) {
|
|
45
|
-
envVars.set(varName, []);
|
|
55
|
+
envVars.set(varName, { locations: [], hasFallback: false });
|
|
46
56
|
}
|
|
47
|
-
envVars.get(varName)!.push(`${relativePath} (serverless config)`);
|
|
57
|
+
envVars.get(varName)!.locations.push(`${relativePath} (serverless config)`);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
return envVars;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
async scanFile(filePath: string): Promise<
|
|
55
|
-
const envVars = new
|
|
64
|
+
async scanFile(filePath: string): Promise<Map<string, boolean>> {
|
|
65
|
+
const envVars = new Map<string, boolean>();
|
|
56
66
|
|
|
57
67
|
try {
|
|
58
68
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
59
69
|
|
|
60
|
-
// Pattern 1: process.env.VAR_NAME
|
|
61
|
-
|
|
70
|
+
// Pattern 1: process.env.VAR_NAME with fallback checks
|
|
71
|
+
// Matches: process.env.VAR || 'default'
|
|
72
|
+
// process.env.VAR ?? 'default'
|
|
73
|
+
// process.env.VAR ? x : y
|
|
74
|
+
const processEnvWithFallbackPattern = /process\.env\.([A-Z_][A-Z0-9_]*)\s*(\|\||&&|\?\?|\?)/g;
|
|
62
75
|
let match;
|
|
63
76
|
|
|
64
|
-
while ((match =
|
|
65
|
-
envVars.
|
|
77
|
+
while ((match = processEnvWithFallbackPattern.exec(content)) !== null) {
|
|
78
|
+
envVars.set(match[1], true); // Has fallback
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
// Pattern 2: process.env
|
|
69
|
-
|
|
81
|
+
// Pattern 2: process.env.VAR in conditional checks
|
|
82
|
+
// Matches: if (process.env.VAR)
|
|
83
|
+
// if (!process.env.VAR)
|
|
84
|
+
const conditionalPattern = /if\s*\(\s*!?\s*process\.env\.([A-Z_][A-Z0-9_]*)\s*\)/g;
|
|
70
85
|
|
|
71
|
-
while ((match =
|
|
72
|
-
envVars.
|
|
86
|
+
while ((match = conditionalPattern.exec(content)) !== null) {
|
|
87
|
+
if (!envVars.has(match[1])) {
|
|
88
|
+
envVars.set(match[1], true); // Has conditional check
|
|
89
|
+
}
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
// Pattern 3: Destructuring - const {
|
|
76
|
-
const
|
|
92
|
+
// Pattern 3: Destructuring with defaults - const { VAR = 'default' } = process.env
|
|
93
|
+
const destructuringWithDefaultPattern = /const\s+\{\s*([^}]+)\s*\}\s*=\s*process\.env/g;
|
|
77
94
|
|
|
78
|
-
while ((match =
|
|
79
|
-
const vars = match[1].split(',').map(v => v.trim()
|
|
95
|
+
while ((match = destructuringWithDefaultPattern.exec(content)) !== null) {
|
|
96
|
+
const vars = match[1].split(',').map(v => v.trim());
|
|
80
97
|
vars.forEach(v => {
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
const parts = v.split('=');
|
|
99
|
+
const varName = parts[0].split(':')[0].trim();
|
|
100
|
+
const hasDefault = parts.length > 1;
|
|
101
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(varName)) {
|
|
102
|
+
if (!envVars.has(varName)) {
|
|
103
|
+
envVars.set(varName, hasDefault);
|
|
104
|
+
}
|
|
83
105
|
}
|
|
84
106
|
});
|
|
85
107
|
}
|
|
86
108
|
|
|
109
|
+
// Pattern 4: Optional chaining - process.env?.VAR
|
|
110
|
+
const optionalChainingPattern = /process\.env\?\.([A-Z_][A-Z0-9_]*)/g;
|
|
111
|
+
|
|
112
|
+
while ((match = optionalChainingPattern.exec(content)) !== null) {
|
|
113
|
+
if (!envVars.has(match[1])) {
|
|
114
|
+
envVars.set(match[1], true); // Has optional chaining
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Pattern 5: process.env['VAR_NAME'] or process.env["VAR_NAME"] with fallback
|
|
119
|
+
const processEnvBracketWithFallbackPattern = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"\]]\s*(\|\||&&|\?\?|\?)/g;
|
|
120
|
+
|
|
121
|
+
while ((match = processEnvBracketWithFallbackPattern.exec(content)) !== null) {
|
|
122
|
+
envVars.set(match[1], true); // Has fallback
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Pattern 6: Basic usage without any safety (process.env.VAR)
|
|
126
|
+
// This should come AFTER all the fallback patterns so we don't override them
|
|
127
|
+
const basicProcessEnvPattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
128
|
+
|
|
129
|
+
while ((match = basicProcessEnvPattern.exec(content)) !== null) {
|
|
130
|
+
if (!envVars.has(match[1])) {
|
|
131
|
+
envVars.set(match[1], false); // No fallback detected
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pattern 7: Basic bracket notation without safety
|
|
136
|
+
const basicBracketPattern = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"\]]/g;
|
|
137
|
+
|
|
138
|
+
while ((match = basicBracketPattern.exec(content)) !== null) {
|
|
139
|
+
if (!envVars.has(match[1])) {
|
|
140
|
+
envVars.set(match[1], false); // No fallback detected
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
87
144
|
} catch (error) {
|
|
88
145
|
console.error(`Error scanning file ${filePath}:`, error);
|
|
89
146
|
}
|
|
@@ -92,9 +149,13 @@ export class CodeScanner {
|
|
|
92
149
|
}
|
|
93
150
|
|
|
94
151
|
async findEnvFiles(): Promise<string[]> {
|
|
152
|
+
const ignorePatterns = this.excludePatterns.map(p =>
|
|
153
|
+
p.includes('*') ? p : `**/${p}/**`
|
|
154
|
+
);
|
|
155
|
+
|
|
95
156
|
const envFiles = await glob('**/.env', {
|
|
96
157
|
cwd: this.rootDir,
|
|
97
|
-
ignore:
|
|
158
|
+
ignore: ignorePatterns,
|
|
98
159
|
absolute: true,
|
|
99
160
|
});
|
|
100
161
|
|
|
@@ -102,9 +163,13 @@ export class CodeScanner {
|
|
|
102
163
|
}
|
|
103
164
|
|
|
104
165
|
async findServerlessFiles(): Promise<string[]> {
|
|
166
|
+
const ignorePatterns = this.excludePatterns.map(p =>
|
|
167
|
+
p.includes('*') ? p : `**/${p}/**`
|
|
168
|
+
);
|
|
169
|
+
|
|
105
170
|
const serverlessFiles = await glob('**/serverless.{yml,yaml}', {
|
|
106
171
|
cwd: this.rootDir,
|
|
107
|
-
ignore:
|
|
172
|
+
ignore: ignorePatterns,
|
|
108
173
|
absolute: true,
|
|
109
174
|
});
|
|
110
175
|
|
|
@@ -120,9 +185,13 @@ export class CodeScanner {
|
|
|
120
185
|
const dirMap = new Map<string, Map<string, string[]>>();
|
|
121
186
|
|
|
122
187
|
// Scan for JavaScript/TypeScript files
|
|
188
|
+
const ignorePatterns = this.excludePatterns.map(p =>
|
|
189
|
+
p.includes('*') ? p : `**/${p}/**`
|
|
190
|
+
);
|
|
191
|
+
|
|
123
192
|
const files = await glob('**/*.{js,ts,jsx,tsx,mjs,cjs}', {
|
|
124
193
|
cwd: this.rootDir,
|
|
125
|
-
ignore:
|
|
194
|
+
ignore: ignorePatterns,
|
|
126
195
|
absolute: true,
|
|
127
196
|
});
|
|
128
197
|
|
|
@@ -131,7 +200,7 @@ export class CodeScanner {
|
|
|
131
200
|
const fileDir = path.dirname(file);
|
|
132
201
|
const relativePath = path.relative(this.rootDir, file);
|
|
133
202
|
|
|
134
|
-
for (const varName of vars) {
|
|
203
|
+
for (const [varName, hasFallback] of vars.entries()) {
|
|
135
204
|
if (!dirMap.has(fileDir)) {
|
|
136
205
|
dirMap.set(fileDir, new Map());
|
|
137
206
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface EnvUsage {
|
|
2
2
|
varName: string;
|
|
3
3
|
locations: string[];
|
|
4
|
+
hasFallback?: boolean; // Whether the usage has a safe fallback/default
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export interface EnvDefinition {
|
|
@@ -13,6 +14,7 @@ export interface EnvDefinition {
|
|
|
13
14
|
|
|
14
15
|
export interface Issue {
|
|
15
16
|
type: 'missing' | 'unused' | 'undocumented';
|
|
17
|
+
severity: 'error' | 'warning' | 'info';
|
|
16
18
|
varName: string;
|
|
17
19
|
details: string;
|
|
18
20
|
locations?: string[];
|
|
@@ -20,7 +22,7 @@ export interface Issue {
|
|
|
20
22
|
|
|
21
23
|
export interface ScanResult {
|
|
22
24
|
issues: Issue[];
|
|
23
|
-
usedVars: Map<string, string[]>;
|
|
25
|
+
usedVars: Map<string, { locations: string[], hasFallback: boolean }>;
|
|
24
26
|
definedVars: Set<string>;
|
|
25
27
|
exampleVars: Set<string>;
|
|
26
28
|
}
|