@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.
Files changed (43) hide show
  1. package/.envguardrc.example.json +1 -0
  2. package/.envguardrc.json +16 -0
  3. package/.github/FUNDING.yml +15 -0
  4. package/LICENSE +1 -1
  5. package/README.md +285 -10
  6. package/dist/analyzer/envAnalyzer.d.ts +8 -2
  7. package/dist/analyzer/envAnalyzer.d.ts.map +1 -1
  8. package/dist/analyzer/envAnalyzer.js +22 -8
  9. package/dist/analyzer/envAnalyzer.js.map +1 -1
  10. package/dist/cli.js +58 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/fix.d.ts.map +1 -1
  13. package/dist/commands/fix.js +32 -7
  14. package/dist/commands/fix.js.map +1 -1
  15. package/dist/commands/install-hook.d.ts +18 -0
  16. package/dist/commands/install-hook.d.ts.map +1 -0
  17. package/dist/commands/install-hook.js +148 -0
  18. package/dist/commands/install-hook.js.map +1 -0
  19. package/dist/commands/scan.d.ts +1 -0
  20. package/dist/commands/scan.d.ts.map +1 -1
  21. package/dist/commands/scan.js +150 -41
  22. package/dist/commands/scan.js.map +1 -1
  23. package/dist/config/configLoader.d.ts +6 -0
  24. package/dist/config/configLoader.d.ts.map +1 -1
  25. package/dist/config/configLoader.js +1 -0
  26. package/dist/config/configLoader.js.map +1 -1
  27. package/dist/scanner/codeScanner.d.ts +5 -2
  28. package/dist/scanner/codeScanner.d.ts.map +1 -1
  29. package/dist/scanner/codeScanner.js +72 -25
  30. package/dist/scanner/codeScanner.js.map +1 -1
  31. package/dist/types.d.ts +6 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/analyzer/envAnalyzer.ts +27 -10
  35. package/src/cli.ts +62 -3
  36. package/src/commands/fix.ts +40 -9
  37. package/src/commands/install-hook.ts +128 -0
  38. package/src/commands/scan.ts +168 -47
  39. package/src/config/configLoader.ts +8 -0
  40. package/src/scanner/codeScanner.ts +97 -28
  41. package/src/types.ts +3 -1
  42. package/test-project/src/lambda2/handler2.js +1 -1
  43. 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: this.excludePatterns.map(p => `**/${p}/**`),
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)!.push(relativePath);
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<Set<string>> {
55
- const envVars = new Set<string>();
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
- const processEnvPattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
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 = processEnvPattern.exec(content)) !== null) {
65
- envVars.add(match[1]);
77
+ while ((match = processEnvWithFallbackPattern.exec(content)) !== null) {
78
+ envVars.set(match[1], true); // Has fallback
66
79
  }
67
80
 
68
- // Pattern 2: process.env['VAR_NAME'] or process.env["VAR_NAME"]
69
- const processEnvBracketPattern = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"\]]/g;
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 = processEnvBracketPattern.exec(content)) !== null) {
72
- envVars.add(match[1]);
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 { VAR_NAME } = process.env
76
- const destructuringPattern = /const\s+\{\s*([^}]+)\s*\}\s*=\s*process\.env/g;
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 = destructuringPattern.exec(content)) !== null) {
79
- const vars = match[1].split(',').map(v => v.trim().split(':')[0].trim());
95
+ while ((match = destructuringWithDefaultPattern.exec(content)) !== null) {
96
+ const vars = match[1].split(',').map(v => v.trim());
80
97
  vars.forEach(v => {
81
- if (/^[A-Z_][A-Z0-9_]*$/.test(v)) {
82
- envVars.add(v);
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: this.excludePatterns.map(p => `**/${p}/**`),
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: this.excludePatterns.map(p => `**/${p}/**`),
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: this.excludePatterns.map(p => `**/${p}/**`),
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
  }
@@ -1,5 +1,5 @@
1
1
 
2
- const OKTA_KEY = process.env.OKTA_DEV_CLIENT_ID;
2
+ const OKTA_KEY = process.env.OKTA_DEV_CLIENT_ID || 'NOT_SET';
3
3
 
4
4
  const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
5
5
 
@@ -1,7 +0,0 @@
1
- {
2
- "ignoreVars": [
3
- "STAGE",
4
- "OKTA_API_PATH"
5
- ],
6
- "strict": false
7
- }