@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Logger } from '../utils/logger';
|
|
4
|
+
|
|
5
|
+
const HOOK_TYPES = ['pre-commit', 'pre-push'] as const;
|
|
6
|
+
type HookType = typeof HOOK_TYPES[number];
|
|
7
|
+
|
|
8
|
+
interface InstallHookOptions {
|
|
9
|
+
type?: HookType;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Install a Git hook that runs envguard before commits or pushes
|
|
15
|
+
*/
|
|
16
|
+
export async function installHookCommand(options: InstallHookOptions = {}) {
|
|
17
|
+
const rootDir = process.cwd();
|
|
18
|
+
const gitDir = path.join(rootDir, '.git');
|
|
19
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
20
|
+
|
|
21
|
+
// Check if this is a git repository
|
|
22
|
+
if (!fs.existsSync(gitDir)) {
|
|
23
|
+
Logger.error('Not a git repository. Please run this command in a git repository.');
|
|
24
|
+
Logger.blank();
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ensure hooks directory exists
|
|
29
|
+
if (!fs.existsSync(hooksDir)) {
|
|
30
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
31
|
+
Logger.success('Created .git/hooks directory');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hookType = options.type || 'pre-commit';
|
|
35
|
+
const hookPath = path.join(hooksDir, hookType);
|
|
36
|
+
|
|
37
|
+
// Check if hook already exists
|
|
38
|
+
if (fs.existsSync(hookPath) && !options.force) {
|
|
39
|
+
Logger.warning(`${hookType} hook already exists.`);
|
|
40
|
+
Logger.info('Use --force to overwrite the existing hook', true);
|
|
41
|
+
Logger.blank();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create the hook script
|
|
46
|
+
const hookContent = generateHookScript(hookType);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
50
|
+
Logger.success(`Installed ${hookType} hook successfully!`);
|
|
51
|
+
Logger.blank();
|
|
52
|
+
Logger.info('The hook will run `envguard check` automatically before each ' +
|
|
53
|
+
(hookType === 'pre-commit' ? 'commit' : 'push'), true);
|
|
54
|
+
Logger.info('To bypass the hook, use: git ' +
|
|
55
|
+
(hookType === 'pre-commit' ? 'commit' : 'push') + ' --no-verify', true);
|
|
56
|
+
Logger.blank();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
Logger.error(`Failed to install hook: ${error}`);
|
|
59
|
+
Logger.blank();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Uninstall a Git hook
|
|
66
|
+
*/
|
|
67
|
+
export async function uninstallHookCommand(options: { type?: HookType } = {}) {
|
|
68
|
+
const rootDir = process.cwd();
|
|
69
|
+
const hookType = options.type || 'pre-commit';
|
|
70
|
+
const hookPath = path.join(rootDir, '.git', 'hooks', hookType);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(hookPath)) {
|
|
73
|
+
Logger.warning(`No ${hookType} hook found.`);
|
|
74
|
+
Logger.blank();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if it's our hook
|
|
79
|
+
const hookContent = fs.readFileSync(hookPath, 'utf-8');
|
|
80
|
+
if (!hookContent.includes('envguard check')) {
|
|
81
|
+
Logger.warning(`The ${hookType} hook exists but was not created by envguard.`);
|
|
82
|
+
Logger.info('Manual removal required if you want to delete it.', true);
|
|
83
|
+
Logger.blank();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(hookPath);
|
|
89
|
+
Logger.success(`Removed ${hookType} hook successfully!`);
|
|
90
|
+
Logger.blank();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
Logger.error(`Failed to remove hook: ${error}`);
|
|
93
|
+
Logger.blank();
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate the hook script content
|
|
100
|
+
*/
|
|
101
|
+
function generateHookScript(hookType: HookType): string {
|
|
102
|
+
const hookMessage = hookType === 'pre-commit' ? 'commit' : 'push';
|
|
103
|
+
|
|
104
|
+
return `#!/bin/sh
|
|
105
|
+
# EnvGuard ${hookType} hook
|
|
106
|
+
# This hook runs envguard to check environment variables before ${hookMessage}
|
|
107
|
+
# To bypass this hook, use: git ${hookMessage} --no-verify
|
|
108
|
+
|
|
109
|
+
echo "Running EnvGuard environment variable check..."
|
|
110
|
+
|
|
111
|
+
# Run envguard check
|
|
112
|
+
npx envguard check
|
|
113
|
+
|
|
114
|
+
# Capture the exit code
|
|
115
|
+
EXIT_CODE=$?
|
|
116
|
+
|
|
117
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
118
|
+
echo ""
|
|
119
|
+
echo "❌ EnvGuard check failed. Please fix the issues above before ${hookMessage}ing."
|
|
120
|
+
echo " Or run: git ${hookMessage} --no-verify to bypass this check."
|
|
121
|
+
echo ""
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
echo "✓ EnvGuard check passed!"
|
|
126
|
+
exit 0
|
|
127
|
+
`;
|
|
128
|
+
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { isKnownRuntimeVar, getRuntimeVarCategory } from '../constants/knownEnvV
|
|
|
10
10
|
import { ConfigLoader } from '../config/configLoader';
|
|
11
11
|
import { Logger } from '../utils/logger';
|
|
12
12
|
|
|
13
|
-
export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
13
|
+
export async function scanCommand(options: { ci?: boolean; strict?: boolean; detectFallbacks?: boolean }) {
|
|
14
14
|
const rootDir = process.cwd();
|
|
15
15
|
|
|
16
16
|
// Load configuration
|
|
@@ -18,11 +18,19 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
18
18
|
|
|
19
19
|
// CLI options override config file
|
|
20
20
|
const strictMode = options.strict !== undefined ? options.strict : config.strict;
|
|
21
|
+
const detectFallbacks = options.detectFallbacks !== undefined ? options.detectFallbacks : (config.detectFallbacks !== undefined ? config.detectFallbacks : true);
|
|
21
22
|
|
|
22
23
|
Logger.startSpinner('Scanning codebase for environment variables...');
|
|
23
24
|
|
|
24
25
|
// Step 1: Find all .env files and serverless.yml files
|
|
25
|
-
const
|
|
26
|
+
const excludePatterns = [
|
|
27
|
+
'node_modules',
|
|
28
|
+
'dist',
|
|
29
|
+
'build',
|
|
30
|
+
'.git',
|
|
31
|
+
...(config.exclude || []),
|
|
32
|
+
];
|
|
33
|
+
const scanner = new CodeScanner(rootDir, excludePatterns);
|
|
26
34
|
const envFiles = await scanner.findEnvFiles();
|
|
27
35
|
const serverlessFiles = await scanner.findServerlessFiles();
|
|
28
36
|
|
|
@@ -55,7 +63,7 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
55
63
|
Logger.info(`Found ${serverlessVars.size} variable(s) in serverless.yml`, true);
|
|
56
64
|
|
|
57
65
|
// Scan code files in this directory to see what's actually used
|
|
58
|
-
const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
|
|
66
|
+
const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner, config.exclude);
|
|
59
67
|
Logger.info(`Found ${usedVars.size} variable(s) used in code`, true);
|
|
60
68
|
Logger.blank();
|
|
61
69
|
|
|
@@ -72,10 +80,10 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
// Check for variables used in code but not defined in serverless.yml
|
|
75
|
-
const missingFromServerless: Array<{ varName: string; locations: string[]; category?: string }> = [];
|
|
83
|
+
const missingFromServerless: Array<{ varName: string; locations: string[]; hasFallback: boolean; category?: string }> = [];
|
|
76
84
|
const skippedRuntimeVars: Array<{ varName: string; category: string }> = [];
|
|
77
85
|
|
|
78
|
-
for (const [varName,
|
|
86
|
+
for (const [varName, usage] of usedVars.entries()) {
|
|
79
87
|
if (!serverlessVars.has(varName)) {
|
|
80
88
|
// In non-strict mode, skip known runtime variables and custom ignore vars
|
|
81
89
|
const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
|
|
@@ -87,17 +95,18 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
87
95
|
skippedRuntimeVars.push({ varName, category });
|
|
88
96
|
}
|
|
89
97
|
} else {
|
|
90
|
-
missingFromServerless.push({ varName, locations });
|
|
98
|
+
missingFromServerless.push({ varName, locations: usage.locations, hasFallback: usage.hasFallback });
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
if (unusedServerlessVars.length > 0) {
|
|
96
|
-
Logger.
|
|
104
|
+
Logger.info('Unused variables in serverless.yml:', true);
|
|
97
105
|
unusedServerlessVars.forEach((varName) => {
|
|
98
|
-
Logger.
|
|
106
|
+
Logger.infoItem(varName, 2);
|
|
99
107
|
allIssues.push({
|
|
100
108
|
type: 'unused',
|
|
109
|
+
severity: 'info',
|
|
101
110
|
varName,
|
|
102
111
|
details: `Defined in serverless.yml but never used in code`,
|
|
103
112
|
});
|
|
@@ -106,20 +115,48 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
if (missingFromServerless.length > 0) {
|
|
109
|
-
|
|
110
|
-
missingFromServerless.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// Group by severity (respect detectFallbacks config)
|
|
119
|
+
const errors = missingFromServerless.filter(item => !detectFallbacks || !item.hasFallback);
|
|
120
|
+
const warnings = missingFromServerless.filter(item => detectFallbacks && item.hasFallback);
|
|
121
|
+
|
|
122
|
+
if (errors.length > 0) {
|
|
123
|
+
Logger.error('Missing from serverless.yml:', true);
|
|
124
|
+
errors.forEach((item) => {
|
|
125
|
+
Logger.errorItem(item.varName, 2);
|
|
126
|
+
if (item.locations && item.locations.length > 0) {
|
|
127
|
+
Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
|
|
128
|
+
}
|
|
129
|
+
const details = (detectFallbacks && item.hasFallback)
|
|
130
|
+
? `Used in code with fallback but not defined in serverless.yml`
|
|
131
|
+
: `Used in code but not defined in serverless.yml`;
|
|
132
|
+
allIssues.push({
|
|
133
|
+
type: 'missing',
|
|
134
|
+
severity: 'error',
|
|
135
|
+
varName: item.varName,
|
|
136
|
+
details,
|
|
137
|
+
locations: item.locations,
|
|
138
|
+
});
|
|
120
139
|
});
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
Logger.blank();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (warnings.length > 0) {
|
|
144
|
+
Logger.warning('Missing from serverless.yml (with fallback):', true);
|
|
145
|
+
warnings.forEach((item) => {
|
|
146
|
+
Logger.warningItem(item.varName, 2);
|
|
147
|
+
if (item.locations && item.locations.length > 0) {
|
|
148
|
+
Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
|
|
149
|
+
}
|
|
150
|
+
allIssues.push({
|
|
151
|
+
type: 'missing',
|
|
152
|
+
severity: 'warning',
|
|
153
|
+
varName: item.varName,
|
|
154
|
+
details: `Used in code with fallback but not defined in serverless.yml`,
|
|
155
|
+
locations: item.locations,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
Logger.blank();
|
|
159
|
+
}
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
|
|
@@ -155,7 +192,26 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
155
192
|
Logger.blank();
|
|
156
193
|
|
|
157
194
|
// Step 3: Scan code files in this directory and subdirectories
|
|
158
|
-
const
|
|
195
|
+
const allUsedVars = await scanDirectoryForVars(rootDir, envDir, scanner, config.exclude);
|
|
196
|
+
|
|
197
|
+
// Filter out ignored variables based on config
|
|
198
|
+
const usedVars = new Map<string, { locations: string[], hasFallback: boolean }>();
|
|
199
|
+
const skippedVarsInScope: Array<{ varName: string; category: string }> = [];
|
|
200
|
+
|
|
201
|
+
for (const [varName, usage] of allUsedVars.entries()) {
|
|
202
|
+
const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
|
|
203
|
+
const isRuntimeVar = isKnownRuntimeVar(varName);
|
|
204
|
+
|
|
205
|
+
// In non-strict mode, skip known runtime variables and custom ignore vars
|
|
206
|
+
if (strictMode || (!isRuntimeVar && !isCustomIgnored)) {
|
|
207
|
+
usedVars.set(varName, usage);
|
|
208
|
+
} else {
|
|
209
|
+
const category = isCustomIgnored ? 'Custom (from config)' : getRuntimeVarCategory(varName);
|
|
210
|
+
if (category) {
|
|
211
|
+
skippedVarsInScope.push({ varName, category });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
159
215
|
|
|
160
216
|
Logger.info(`Found ${usedVars.size} variable(s) used in this scope`, true);
|
|
161
217
|
|
|
@@ -170,17 +226,19 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
170
226
|
Logger.blank();
|
|
171
227
|
|
|
172
228
|
// Step 6: Analyze and find issues
|
|
173
|
-
const result = analyzer.analyze(usedVars, definedVars, exampleVars);
|
|
229
|
+
const result = analyzer.analyze(usedVars, definedVars, exampleVars, detectFallbacks);
|
|
174
230
|
|
|
175
231
|
if (result.issues.length > 0) {
|
|
176
|
-
// Group issues by type
|
|
177
|
-
const
|
|
232
|
+
// Group issues by type and severity
|
|
233
|
+
const missingErrors = result.issues.filter(i => i.type === 'missing' && i.severity === 'error');
|
|
234
|
+
const missingWarnings = result.issues.filter(i => i.type === 'missing' && i.severity === 'warning');
|
|
178
235
|
const unusedIssues = result.issues.filter(i => i.type === 'unused');
|
|
179
|
-
const
|
|
236
|
+
const undocumentedWarnings = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'warning');
|
|
237
|
+
const undocumentedInfo = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'info');
|
|
180
238
|
|
|
181
|
-
if (
|
|
239
|
+
if (missingErrors.length > 0) {
|
|
182
240
|
Logger.error('Missing from .env:', true);
|
|
183
|
-
|
|
241
|
+
missingErrors.forEach((issue) => {
|
|
184
242
|
Logger.errorItem(issue.varName, 2);
|
|
185
243
|
if (issue.locations && issue.locations.length > 0) {
|
|
186
244
|
Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
|
|
@@ -189,17 +247,36 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
189
247
|
Logger.blank();
|
|
190
248
|
}
|
|
191
249
|
|
|
250
|
+
if (missingWarnings.length > 0) {
|
|
251
|
+
Logger.warning('Missing from .env (with fallback):', true);
|
|
252
|
+
missingWarnings.forEach((issue) => {
|
|
253
|
+
Logger.warningItem(issue.varName, 2);
|
|
254
|
+
if (issue.locations && issue.locations.length > 0) {
|
|
255
|
+
Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
Logger.blank();
|
|
259
|
+
}
|
|
260
|
+
|
|
192
261
|
if (unusedIssues.length > 0) {
|
|
193
|
-
Logger.
|
|
262
|
+
Logger.info('Unused variables:', true);
|
|
194
263
|
unusedIssues.forEach((issue) => {
|
|
264
|
+
Logger.infoItem(issue.varName, 2);
|
|
265
|
+
});
|
|
266
|
+
Logger.blank();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (undocumentedWarnings.length > 0) {
|
|
270
|
+
Logger.warning('Missing from .env.example:', true);
|
|
271
|
+
undocumentedWarnings.forEach((issue) => {
|
|
195
272
|
Logger.warningItem(issue.varName, 2);
|
|
196
273
|
});
|
|
197
274
|
Logger.blank();
|
|
198
275
|
}
|
|
199
276
|
|
|
200
|
-
if (
|
|
201
|
-
Logger.info('Missing from .env.example:', true);
|
|
202
|
-
|
|
277
|
+
if (undocumentedInfo.length > 0) {
|
|
278
|
+
Logger.info('Missing from .env.example (with fallback):', true);
|
|
279
|
+
undocumentedInfo.forEach((issue) => {
|
|
203
280
|
Logger.infoItem(issue.varName, 2);
|
|
204
281
|
});
|
|
205
282
|
Logger.blank();
|
|
@@ -210,6 +287,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
210
287
|
Logger.success('No issues in this directory', true);
|
|
211
288
|
Logger.blank();
|
|
212
289
|
}
|
|
290
|
+
|
|
291
|
+
// Show skipped variables in non-strict mode
|
|
292
|
+
if (!strictMode && skippedVarsInScope.length > 0) {
|
|
293
|
+
Logger.info('Skipped known runtime/ignored variables (use --strict to show):', true);
|
|
294
|
+
// Group by category
|
|
295
|
+
const grouped = new Map<string, string[]>();
|
|
296
|
+
for (const { varName, category } of skippedVarsInScope) {
|
|
297
|
+
if (!grouped.has(category)) {
|
|
298
|
+
grouped.set(category, []);
|
|
299
|
+
}
|
|
300
|
+
grouped.get(category)!.push(varName);
|
|
301
|
+
}
|
|
302
|
+
for (const [category, vars] of grouped.entries()) {
|
|
303
|
+
Logger.info(`${category}: ${vars.join(', ')}`, true);
|
|
304
|
+
}
|
|
305
|
+
Logger.blank();
|
|
306
|
+
}
|
|
213
307
|
}
|
|
214
308
|
|
|
215
309
|
// Display summary
|
|
@@ -219,8 +313,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
219
313
|
return { success: true, issues: [] };
|
|
220
314
|
}
|
|
221
315
|
|
|
316
|
+
// Count issues by severity
|
|
317
|
+
const errorCount = allIssues.filter(i => i.severity === 'error').length;
|
|
318
|
+
const warningCount = allIssues.filter(i => i.severity === 'warning').length;
|
|
319
|
+
const infoCount = allIssues.filter(i => i.severity === 'info').length;
|
|
320
|
+
|
|
222
321
|
Logger.blank();
|
|
223
|
-
|
|
322
|
+
if (errorCount > 0) {
|
|
323
|
+
Logger.error(`Errors: ${errorCount}`, false);
|
|
324
|
+
}
|
|
325
|
+
if (warningCount > 0) {
|
|
326
|
+
Logger.warning(`Warnings: ${warningCount}`, false);
|
|
327
|
+
}
|
|
328
|
+
if (infoCount > 0) {
|
|
329
|
+
Logger.info(`Info: ${infoCount}`, false);
|
|
330
|
+
}
|
|
331
|
+
Logger.blank();
|
|
332
|
+
Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length + serverlessFiles.length} location(s)`);
|
|
224
333
|
Logger.blank();
|
|
225
334
|
|
|
226
335
|
// Suggest fix
|
|
@@ -242,28 +351,34 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
|
|
|
242
351
|
async function scanDirectoryForVars(
|
|
243
352
|
rootDir: string,
|
|
244
353
|
targetDir: string,
|
|
245
|
-
scanner: CodeScanner
|
|
246
|
-
|
|
247
|
-
|
|
354
|
+
scanner: CodeScanner,
|
|
355
|
+
excludePatterns: string[] = []
|
|
356
|
+
): Promise<Map<string, { locations: string[], hasFallback: boolean }>> {
|
|
357
|
+
const envVars = new Map<string, { locations: string[], hasFallback: boolean }>();
|
|
248
358
|
|
|
249
359
|
// Find all code files in this directory and subdirectories
|
|
250
360
|
const relativeDir = path.relative(rootDir, targetDir);
|
|
251
361
|
const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
|
|
252
362
|
|
|
363
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
|
|
364
|
+
const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
|
|
365
|
+
|
|
253
366
|
const files = await glob(pattern, {
|
|
254
367
|
cwd: rootDir,
|
|
255
|
-
ignore: [
|
|
368
|
+
ignore: [...defaultIgnore, ...customIgnore],
|
|
256
369
|
absolute: true,
|
|
257
370
|
});
|
|
258
371
|
|
|
259
372
|
for (const file of files) {
|
|
260
373
|
const vars = await scanner.scanFile(file);
|
|
261
|
-
for (const varName of vars) {
|
|
374
|
+
for (const [varName, hasFallback] of vars.entries()) {
|
|
262
375
|
const relativePath = path.relative(rootDir, file);
|
|
263
376
|
if (!envVars.has(varName)) {
|
|
264
|
-
envVars.set(varName, []);
|
|
377
|
+
envVars.set(varName, { locations: [], hasFallback: false });
|
|
265
378
|
}
|
|
266
|
-
envVars.get(varName)
|
|
379
|
+
const entry = envVars.get(varName)!;
|
|
380
|
+
entry.locations.push(relativePath);
|
|
381
|
+
entry.hasFallback = entry.hasFallback || hasFallback;
|
|
267
382
|
}
|
|
268
383
|
}
|
|
269
384
|
|
|
@@ -274,28 +389,34 @@ async function scanDirectoryForVars(
|
|
|
274
389
|
async function scanDirectoryForCodeVars(
|
|
275
390
|
rootDir: string,
|
|
276
391
|
targetDir: string,
|
|
277
|
-
scanner: CodeScanner
|
|
278
|
-
|
|
279
|
-
|
|
392
|
+
scanner: CodeScanner,
|
|
393
|
+
excludePatterns: string[] = []
|
|
394
|
+
): Promise<Map<string, { locations: string[], hasFallback: boolean }>> {
|
|
395
|
+
const envVars = new Map<string, { locations: string[], hasFallback: boolean }>();
|
|
280
396
|
|
|
281
397
|
// Find all code files in this directory only (not subdirectories for serverless)
|
|
282
398
|
const relativeDir = path.relative(rootDir, targetDir);
|
|
283
399
|
const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
|
|
284
400
|
|
|
401
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
|
|
402
|
+
const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
|
|
403
|
+
|
|
285
404
|
const files = await glob(pattern, {
|
|
286
405
|
cwd: rootDir,
|
|
287
|
-
ignore: [
|
|
406
|
+
ignore: [...defaultIgnore, ...customIgnore],
|
|
288
407
|
absolute: true,
|
|
289
408
|
});
|
|
290
409
|
|
|
291
410
|
for (const file of files) {
|
|
292
411
|
const vars = await scanner.scanFile(file);
|
|
293
|
-
for (const varName of vars) {
|
|
412
|
+
for (const [varName, hasFallback] of vars.entries()) {
|
|
294
413
|
const relativePath = path.relative(rootDir, file);
|
|
295
414
|
if (!envVars.has(varName)) {
|
|
296
|
-
envVars.set(varName, []);
|
|
415
|
+
envVars.set(varName, { locations: [], hasFallback: false });
|
|
297
416
|
}
|
|
298
|
-
envVars.get(varName)
|
|
417
|
+
const entry = envVars.get(varName)!;
|
|
418
|
+
entry.locations.push(relativePath);
|
|
419
|
+
entry.hasFallback = entry.hasFallback || hasFallback;
|
|
299
420
|
}
|
|
300
421
|
}
|
|
301
422
|
|
|
@@ -17,12 +17,20 @@ export interface EnvGuardConfig {
|
|
|
17
17
|
* Enable strict mode by default
|
|
18
18
|
*/
|
|
19
19
|
strict?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect fallback patterns in code (||, ??, conditionals, etc.)
|
|
23
|
+
* When enabled, variables with fallbacks are treated as warnings instead of errors
|
|
24
|
+
* Default: true
|
|
25
|
+
*/
|
|
26
|
+
detectFallbacks?: boolean;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const DEFAULT_CONFIG: EnvGuardConfig = {
|
|
23
30
|
ignoreVars: [],
|
|
24
31
|
exclude: [],
|
|
25
32
|
strict: false,
|
|
33
|
+
detectFallbacks: true,
|
|
26
34
|
};
|
|
27
35
|
|
|
28
36
|
const CONFIG_FILE_NAMES = [
|