envprobe 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 +21 -0
- package/README.md +316 -0
- package/bin/envcheck.js +68 -0
- package/package.json +49 -0
- package/src/analyzer.js +179 -0
- package/src/autocomplete.js +135 -0
- package/src/cache.js +114 -0
- package/src/cli.js +606 -0
- package/src/config.js +118 -0
- package/src/formatters/github.js +164 -0
- package/src/formatters/json.js +114 -0
- package/src/formatters/table.js +92 -0
- package/src/formatters/text.js +198 -0
- package/src/ignore.js +313 -0
- package/src/parser.js +119 -0
- package/src/plugins.js +138 -0
- package/src/progress.js +181 -0
- package/src/repl.js +416 -0
- package/src/scanner.js +182 -0
- package/src/scanners/go.js +89 -0
- package/src/scanners/javascript.js +93 -0
- package/src/scanners/python.js +97 -0
- package/src/scanners/ruby.js +90 -0
- package/src/scanners/rust.js +103 -0
- package/src/scanners/shell.js +125 -0
- package/src/security.js +411 -0
- package/src/suggestions.js +154 -0
- package/src/utils.js +57 -0
- package/src/watch.js +131 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell/Bash Scanner
|
|
3
|
+
* Detects environment variable references in shell script files
|
|
4
|
+
* Supports: $VAR_NAME, ${VAR_NAME}
|
|
5
|
+
*
|
|
6
|
+
* Note: Shell scripts may produce more false positives than other languages
|
|
7
|
+
* due to the prevalence of variable usage in shell scripting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regex patterns for detecting environment variable references
|
|
12
|
+
*
|
|
13
|
+
* Pattern 1: $VAR_NAME (simple variable expansion)
|
|
14
|
+
* - Matches: $DATABASE_URL, $API_KEY
|
|
15
|
+
* - Must be followed by a non-word character or end of string to avoid partial matches
|
|
16
|
+
* - More prone to false positives (e.g., $1, $?, special vars)
|
|
17
|
+
* - We filter these out by requiring uppercase start
|
|
18
|
+
* - This pattern is checked FIRST to maintain consistent ordering in results
|
|
19
|
+
*
|
|
20
|
+
* Pattern 2: ${VAR_NAME} (braced variable expansion)
|
|
21
|
+
* - Matches: ${DATABASE_URL}, ${API_KEY}
|
|
22
|
+
* - Also matches parameter expansion: ${VAR:-default}, ${VAR:=default}, ${VAR:?error}
|
|
23
|
+
* - This is the preferred form and less ambiguous
|
|
24
|
+
* - Captures the variable name inside the braces
|
|
25
|
+
*
|
|
26
|
+
* Both patterns work within double quotes, which is common in shell scripts.
|
|
27
|
+
* Single quotes prevent variable expansion in shell, but we still detect them
|
|
28
|
+
* as they might indicate configuration that needs documentation.
|
|
29
|
+
*/
|
|
30
|
+
const PATTERNS = [
|
|
31
|
+
// Simple expansion: $VAR_NAME
|
|
32
|
+
// Uses word boundary or lookahead to ensure we capture the full variable name
|
|
33
|
+
// This pattern is checked FIRST to maintain consistent ordering in results
|
|
34
|
+
/\$([A-Z_][A-Z0-9_]*)(?=\W|$)/g,
|
|
35
|
+
|
|
36
|
+
// Braced expansion: ${VAR_NAME} with optional parameter expansion operators
|
|
37
|
+
// Matches: ${VAR}, ${VAR:-default}, ${VAR:=default}, ${VAR:?error}, ${VAR:+value}
|
|
38
|
+
// Captures the variable name between the braces, before any colon or closing brace
|
|
39
|
+
/\$\{([A-Z_][A-Z0-9_]*)(?:[:\}])/g,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Supported file extensions for Shell/Bash scripts
|
|
44
|
+
*/
|
|
45
|
+
const SUPPORTED_EXTENSIONS = ['.sh', '.bash', '.zsh'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan file content for environment variable references
|
|
49
|
+
* @param {string} content - File content to scan
|
|
50
|
+
* @param {string} filePath - Path to the file being scanned
|
|
51
|
+
* @returns {Array<{varName: string, filePath: string, lineNumber: number, pattern: string}>}
|
|
52
|
+
*/
|
|
53
|
+
export function scan(content, filePath) {
|
|
54
|
+
const references = [];
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
references.push(...scanLine(lines[i], filePath, i + 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return references;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function scanLine(line, filePath, lineNumber) {
|
|
65
|
+
const references = [];
|
|
66
|
+
const seenPerLine = new Set();
|
|
67
|
+
|
|
68
|
+
for (const pattern of PATTERNS) {
|
|
69
|
+
pattern.lastIndex = 0;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
72
|
+
const varName = match[1];
|
|
73
|
+
const matchedPattern = match[0];
|
|
74
|
+
|
|
75
|
+
if (seenPerLine.has(varName)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (varName && isValidEnvVarName(varName)) {
|
|
80
|
+
references.push({
|
|
81
|
+
varName,
|
|
82
|
+
filePath,
|
|
83
|
+
lineNumber,
|
|
84
|
+
pattern: matchedPattern,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
seenPerLine.add(varName);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return references;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate environment variable name
|
|
97
|
+
* Must start with letter or underscore, contain only uppercase letters, digits, and underscores
|
|
98
|
+
*
|
|
99
|
+
* This helps filter out shell special variables like:
|
|
100
|
+
* - Positional parameters: $1, $2, $@, $*
|
|
101
|
+
* - Special variables: $?, $!, $-
|
|
102
|
+
* - Lowercase local variables: $var, $my_var
|
|
103
|
+
*
|
|
104
|
+
* @param {string} varName - Variable name to validate
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isValidEnvVarName(varName) {
|
|
108
|
+
return /^[A-Z_][A-Z0-9_]*$/.test(varName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get supported file extensions
|
|
113
|
+
* @returns {string[]}
|
|
114
|
+
*/
|
|
115
|
+
export function getSupportedExtensions() {
|
|
116
|
+
return SUPPORTED_EXTENSIONS;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get regex patterns used for scanning
|
|
121
|
+
* @returns {RegExp[]}
|
|
122
|
+
*/
|
|
123
|
+
export function getPatterns() {
|
|
124
|
+
return PATTERNS;
|
|
125
|
+
}
|
package/src/security.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Utilities Module
|
|
3
|
+
*
|
|
4
|
+
* Provides security functions for:
|
|
5
|
+
* - Path sanitization and validation
|
|
6
|
+
* - Input validation and sanitization
|
|
7
|
+
* - Pattern validation
|
|
8
|
+
* - Resource limits
|
|
9
|
+
* - Safe file operations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { isValidEnvVarName } from './utils.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maximum file size to process (10MB)
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of files to scan
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_FILES_TO_SCAN = 100000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Maximum directory depth
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_DIRECTORY_DEPTH = 100;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maximum pattern length
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_PATTERN_LENGTH = 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maximum path length
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_PATH_LENGTH = 4096;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sanitize a file path to prevent directory traversal attacks
|
|
42
|
+
*
|
|
43
|
+
* @param {string} filePath - Path to sanitize
|
|
44
|
+
* @param {string} basePath - Base directory path (optional)
|
|
45
|
+
* @returns {string} Sanitized path
|
|
46
|
+
* @throws {Error} If path is invalid or attempts traversal
|
|
47
|
+
*/
|
|
48
|
+
export function sanitizePath(filePath, basePath = process.cwd()) {
|
|
49
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
50
|
+
throw new Error('Invalid file path');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for null bytes
|
|
54
|
+
if (filePath.includes('\x00')) {
|
|
55
|
+
throw new Error('Path contains null bytes');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check path length
|
|
59
|
+
if (filePath.length > MAX_PATH_LENGTH) {
|
|
60
|
+
throw new Error(`Path too long (max ${MAX_PATH_LENGTH} characters)`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve to absolute path
|
|
64
|
+
const resolvedPath = path.resolve(basePath, filePath);
|
|
65
|
+
const resolvedBase = path.resolve(basePath);
|
|
66
|
+
|
|
67
|
+
// Ensure path is within base directory
|
|
68
|
+
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
69
|
+
throw new Error('Path traversal detected');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return resolvedPath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate and sanitize a glob pattern
|
|
77
|
+
*
|
|
78
|
+
* @param {string} pattern - Glob pattern to validate
|
|
79
|
+
* @returns {string} Sanitized pattern
|
|
80
|
+
* @throws {Error} If pattern is invalid or malicious
|
|
81
|
+
*/
|
|
82
|
+
export function sanitizePattern(pattern) {
|
|
83
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
84
|
+
throw new Error('Invalid pattern');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check pattern length
|
|
88
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
89
|
+
throw new Error(`Pattern too long (max ${MAX_PATTERN_LENGTH} characters)`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for null bytes
|
|
93
|
+
if (pattern.includes('\x00')) {
|
|
94
|
+
throw new Error('Pattern contains null bytes');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for shell metacharacters that could cause command injection
|
|
98
|
+
const dangerousChars = /[;&|`$()]/;
|
|
99
|
+
if (dangerousChars.test(pattern)) {
|
|
100
|
+
throw new Error('Pattern contains dangerous characters');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for ReDoS patterns (basic check)
|
|
104
|
+
const redosPatterns = [
|
|
105
|
+
/(\(.*\+\))+/, // (a+)+
|
|
106
|
+
/(\(.*\*\))+/, // (a*)*
|
|
107
|
+
/(\(.*\|\))+/ // (a|b)+
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
for (const redosPattern of redosPatterns) {
|
|
111
|
+
if (redosPattern.test(pattern)) {
|
|
112
|
+
throw new Error('Pattern may cause ReDoS');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return pattern;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sanitize command-line argument
|
|
121
|
+
*
|
|
122
|
+
* @param {string} arg - Argument to sanitize
|
|
123
|
+
* @returns {string} Sanitized argument
|
|
124
|
+
* @throws {Error} If argument is invalid or malicious
|
|
125
|
+
*/
|
|
126
|
+
export function sanitizeArgument(arg) {
|
|
127
|
+
if (typeof arg !== 'string') {
|
|
128
|
+
throw new Error('Invalid argument type');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for null bytes
|
|
132
|
+
if (arg.includes('\x00')) {
|
|
133
|
+
throw new Error('Argument contains null bytes');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for shell metacharacters
|
|
137
|
+
const shellMetachars = /[;&|`$()<>]/;
|
|
138
|
+
if (shellMetachars.test(arg)) {
|
|
139
|
+
throw new Error('Argument contains shell metacharacters');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for newlines (command injection)
|
|
143
|
+
if (arg.includes('\n') || arg.includes('\r')) {
|
|
144
|
+
throw new Error('Argument contains newline characters');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return arg;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate environment variable name
|
|
152
|
+
*
|
|
153
|
+
* @param {string} varName - Variable name to validate
|
|
154
|
+
* @returns {boolean} True if valid
|
|
155
|
+
*/
|
|
156
|
+
export function validateEnvVarName(varName) {
|
|
157
|
+
if (!varName || typeof varName !== 'string') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check length
|
|
162
|
+
if (varName.length > 255) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Use existing validation
|
|
167
|
+
return isValidEnvVarName(varName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sanitize error message to prevent information leakage
|
|
172
|
+
*
|
|
173
|
+
* @param {Error} error - Error object
|
|
174
|
+
* @param {boolean} verbose - Include more details
|
|
175
|
+
* @returns {string} Sanitized error message
|
|
176
|
+
*/
|
|
177
|
+
export function sanitizeErrorMessage(error, verbose = false) {
|
|
178
|
+
if (!error) {
|
|
179
|
+
return 'Unknown error';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let message = error.message || 'Unknown error';
|
|
183
|
+
|
|
184
|
+
if (!verbose) {
|
|
185
|
+
// Remove absolute paths
|
|
186
|
+
message = message.replace(/\/[^\s]+/g, '[path]');
|
|
187
|
+
message = message.replace(/[A-Z]:\\[^\s]+/g, '[path]');
|
|
188
|
+
|
|
189
|
+
// Remove potential secrets (anything that looks like a key/token)
|
|
190
|
+
message = message.replace(/[a-zA-Z0-9_-]{32,}/g, '[redacted]');
|
|
191
|
+
|
|
192
|
+
// Limit message length
|
|
193
|
+
if (message.length > 200) {
|
|
194
|
+
message = message.substring(0, 200) + '...';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return message;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if file size is within limits
|
|
203
|
+
*
|
|
204
|
+
* @param {number} size - File size in bytes
|
|
205
|
+
* @returns {boolean} True if within limits
|
|
206
|
+
*/
|
|
207
|
+
export function isFileSizeValid(size) {
|
|
208
|
+
return typeof size === 'number' && size >= 0 && size <= MAX_FILE_SIZE;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if directory depth is within limits
|
|
213
|
+
*
|
|
214
|
+
* @param {string} filePath - File path to check
|
|
215
|
+
* @param {string} basePath - Base directory path
|
|
216
|
+
* @returns {boolean} True if within limits
|
|
217
|
+
*/
|
|
218
|
+
export function isDepthValid(filePath, basePath = process.cwd()) {
|
|
219
|
+
const relativePath = path.relative(basePath, filePath);
|
|
220
|
+
const depth = relativePath.split(path.sep).length;
|
|
221
|
+
return depth <= MAX_DIRECTORY_DEPTH;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Rate limiter for file operations
|
|
226
|
+
*/
|
|
227
|
+
export class RateLimiter {
|
|
228
|
+
constructor(maxOperations = 1000, windowMs = 1000) {
|
|
229
|
+
this.maxOperations = maxOperations;
|
|
230
|
+
this.windowMs = windowMs;
|
|
231
|
+
this.operations = [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if operation is allowed
|
|
236
|
+
* @returns {boolean} True if allowed
|
|
237
|
+
*/
|
|
238
|
+
allowOperation() {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
|
|
241
|
+
// Remove old operations outside window
|
|
242
|
+
this.operations = this.operations.filter(
|
|
243
|
+
time => now - time < this.windowMs
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Check if under limit
|
|
247
|
+
if (this.operations.length >= this.maxOperations) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Record operation
|
|
252
|
+
this.operations.push(now);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Reset rate limiter
|
|
258
|
+
*/
|
|
259
|
+
reset() {
|
|
260
|
+
this.operations = [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Timeout wrapper for async operations
|
|
266
|
+
*
|
|
267
|
+
* @param {Promise} promise - Promise to wrap
|
|
268
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
269
|
+
* @returns {Promise} Promise that rejects on timeout
|
|
270
|
+
*/
|
|
271
|
+
export function withTimeout(promise, timeoutMs = 30000) {
|
|
272
|
+
return Promise.race([
|
|
273
|
+
promise,
|
|
274
|
+
new Promise((_, reject) =>
|
|
275
|
+
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
|
|
276
|
+
)
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate configuration object
|
|
282
|
+
*
|
|
283
|
+
* @param {Object} config - Configuration to validate
|
|
284
|
+
* @returns {Object} Validated configuration
|
|
285
|
+
* @throws {Error} If configuration is invalid
|
|
286
|
+
*/
|
|
287
|
+
export function validateConfiguration(config) {
|
|
288
|
+
if (!config || typeof config !== 'object') {
|
|
289
|
+
throw new Error('Invalid configuration');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check for prototype pollution - but allow 'path' property
|
|
293
|
+
const dangerousProps = ['__proto__', 'constructor', 'prototype'];
|
|
294
|
+
for (const prop of dangerousProps) {
|
|
295
|
+
if (prop in config) {
|
|
296
|
+
throw new Error('Configuration contains dangerous properties');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Validate each property
|
|
301
|
+
const validated = {};
|
|
302
|
+
|
|
303
|
+
if (config.path !== undefined) {
|
|
304
|
+
if (typeof config.path !== 'string') {
|
|
305
|
+
throw new Error('Invalid path in configuration');
|
|
306
|
+
}
|
|
307
|
+
validated.path = config.path;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (config.envFile !== undefined) {
|
|
311
|
+
if (typeof config.envFile !== 'string') {
|
|
312
|
+
throw new Error('Invalid envFile in configuration');
|
|
313
|
+
}
|
|
314
|
+
validated.envFile = config.envFile;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (config.format !== undefined) {
|
|
318
|
+
if (!['text', 'json', 'github'].includes(config.format)) {
|
|
319
|
+
throw new Error('Invalid format in configuration');
|
|
320
|
+
}
|
|
321
|
+
validated.format = config.format;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (config.failOn !== undefined) {
|
|
325
|
+
if (!['missing', 'unused', 'undocumented', 'all', 'none'].includes(config.failOn)) {
|
|
326
|
+
throw new Error('Invalid failOn in configuration');
|
|
327
|
+
}
|
|
328
|
+
validated.failOn = config.failOn;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (config.ignore !== undefined) {
|
|
332
|
+
if (!Array.isArray(config.ignore)) {
|
|
333
|
+
throw new Error('Invalid ignore in configuration');
|
|
334
|
+
}
|
|
335
|
+
validated.ignore = config.ignore.filter(p => typeof p === 'string');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (config.noColor !== undefined) {
|
|
339
|
+
validated.noColor = Boolean(config.noColor);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (config.quiet !== undefined) {
|
|
343
|
+
validated.quiet = Boolean(config.quiet);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (config.watch !== undefined) {
|
|
347
|
+
validated.watch = Boolean(config.watch);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (config.suggestions !== undefined) {
|
|
351
|
+
validated.suggestions = Boolean(config.suggestions);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (config.progress !== undefined) {
|
|
355
|
+
validated.progress = Boolean(config.progress);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (config.fix !== undefined) {
|
|
359
|
+
validated.fix = Boolean(config.fix);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return validated;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Escape special characters for safe display
|
|
367
|
+
*
|
|
368
|
+
* @param {string} str - String to escape
|
|
369
|
+
* @returns {string} Escaped string
|
|
370
|
+
*/
|
|
371
|
+
export function escapeForDisplay(str) {
|
|
372
|
+
if (typeof str !== 'string') {
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return str
|
|
377
|
+
.replace(/&/g, '&')
|
|
378
|
+
.replace(/</g, '<')
|
|
379
|
+
.replace(/>/g, '>')
|
|
380
|
+
.replace(/"/g, '"')
|
|
381
|
+
.replace(/'/g, ''')
|
|
382
|
+
.replace(/\//g, '/');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if path is safe to access
|
|
387
|
+
*
|
|
388
|
+
* @param {string} filePath - Path to check
|
|
389
|
+
* @returns {boolean} True if safe
|
|
390
|
+
*/
|
|
391
|
+
export function isSafePath(filePath) {
|
|
392
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check for dangerous patterns
|
|
397
|
+
const dangerousPatterns = [
|
|
398
|
+
/\.\./, // Parent directory
|
|
399
|
+
/^\/etc\//, // System files
|
|
400
|
+
/^\/root\//, // Root home
|
|
401
|
+
/^\/sys\//, // System files
|
|
402
|
+
/^\/proc\//, // Process files
|
|
403
|
+
/^C:\\Windows/i, // Windows system
|
|
404
|
+
/^C:\\Program/i, // Program files
|
|
405
|
+
/\.ssh/, // SSH keys
|
|
406
|
+
/\.aws/, // AWS credentials
|
|
407
|
+
/\.env$/ // Actual env files (not .env.example)
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
return !dangerousPatterns.some(pattern => pattern.test(filePath));
|
|
411
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent suggestions and fixes for common issues
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Analyze issues and provide actionable suggestions
|
|
7
|
+
*/
|
|
8
|
+
export function generateSuggestions(result) {
|
|
9
|
+
const suggestions = [];
|
|
10
|
+
|
|
11
|
+
// Missing variables suggestions
|
|
12
|
+
if (result.missing.length > 0) {
|
|
13
|
+
suggestions.push({
|
|
14
|
+
type: 'missing',
|
|
15
|
+
severity: 'error',
|
|
16
|
+
message: `Found ${result.missing.length} missing environment variable(s)`,
|
|
17
|
+
action: 'Add these variables to your .env.example file:',
|
|
18
|
+
items: result.missing.map(m => ({
|
|
19
|
+
variable: m.varName,
|
|
20
|
+
locations: m.locations.map(l => `${l.filePath}:${l.lineNumber}`),
|
|
21
|
+
suggestion: `${m.varName}=`,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Unused variables suggestions
|
|
27
|
+
if (result.unused.length > 0) {
|
|
28
|
+
suggestions.push({
|
|
29
|
+
type: 'unused',
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
message: `Found ${result.unused.length} unused environment variable(s)`,
|
|
32
|
+
action: 'Consider removing these from .env.example or use them in your code:',
|
|
33
|
+
items: result.unused.map(u => ({
|
|
34
|
+
variable: u.varName,
|
|
35
|
+
suggestion: `Remove ${u.varName} from .env.example or add usage in code`,
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Undocumented variables suggestions
|
|
41
|
+
if (result.undocumented.length > 0) {
|
|
42
|
+
suggestions.push({
|
|
43
|
+
type: 'undocumented',
|
|
44
|
+
severity: 'info',
|
|
45
|
+
message: `Found ${result.undocumented.length} undocumented environment variable(s)`,
|
|
46
|
+
action: 'Add comments to document these variables:',
|
|
47
|
+
items: result.undocumented.map(u => ({
|
|
48
|
+
variable: u.varName,
|
|
49
|
+
suggestion: `# Description of ${u.varName}\n${u.varName}=`,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return suggestions;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate auto-fix content for .env.example
|
|
59
|
+
*/
|
|
60
|
+
export function generateEnvExampleFix(result, existingContent = '') {
|
|
61
|
+
const lines = existingContent.split('\n');
|
|
62
|
+
const additions = [];
|
|
63
|
+
|
|
64
|
+
// Add missing variables
|
|
65
|
+
for (const missing of result.missing) {
|
|
66
|
+
const example = inferExampleValue(missing.varName);
|
|
67
|
+
additions.push(`# ${missing.varName} - Add description here`);
|
|
68
|
+
additions.push(`${missing.varName}=${example}`);
|
|
69
|
+
additions.push('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [...lines, '', '# Auto-generated additions', ...additions].join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Infer example value based on variable name
|
|
77
|
+
*/
|
|
78
|
+
function inferExampleValue(varName) {
|
|
79
|
+
const patterns = {
|
|
80
|
+
PORT: '3000',
|
|
81
|
+
HOST: 'localhost',
|
|
82
|
+
URL: 'https://example.com',
|
|
83
|
+
API_KEY: 'your_api_key_here',
|
|
84
|
+
SECRET: 'your_secret_here',
|
|
85
|
+
TOKEN: 'your_token_here',
|
|
86
|
+
PASSWORD: 'your_password_here',
|
|
87
|
+
DATABASE: 'postgres://localhost:5432/dbname',
|
|
88
|
+
REDIS: 'redis://localhost:6379',
|
|
89
|
+
EMAIL: 'user@example.com',
|
|
90
|
+
DEBUG: 'false',
|
|
91
|
+
NODE_ENV: 'development',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const [pattern, value] of Object.entries(patterns)) {
|
|
95
|
+
if (varName.toUpperCase().includes(pattern)) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 'your_value_here';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Suggest similar variable names (typo detection)
|
|
105
|
+
*/
|
|
106
|
+
export function findSimilarVariables(varName, definedVars) {
|
|
107
|
+
const similar = [];
|
|
108
|
+
|
|
109
|
+
for (const defined of definedVars) {
|
|
110
|
+
const distance = levenshteinDistance(varName.toLowerCase(), defined.toLowerCase());
|
|
111
|
+
const maxLength = Math.max(varName.length, defined.length);
|
|
112
|
+
const similarity = 1 - distance / maxLength;
|
|
113
|
+
|
|
114
|
+
if (similarity > 0.7 && similarity < 1) {
|
|
115
|
+
similar.push({
|
|
116
|
+
name: defined,
|
|
117
|
+
similarity: Math.round(similarity * 100),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return similar.sort((a, b) => b.similarity - a.similarity);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Calculate Levenshtein distance between two strings
|
|
127
|
+
*/
|
|
128
|
+
function levenshteinDistance(str1, str2) {
|
|
129
|
+
const matrix = [];
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
132
|
+
matrix[i] = [i];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
136
|
+
matrix[0][j] = j;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
140
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
141
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
142
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
143
|
+
} else {
|
|
144
|
+
matrix[i][j] = Math.min(
|
|
145
|
+
matrix[i - 1][j - 1] + 1,
|
|
146
|
+
matrix[i][j - 1] + 1,
|
|
147
|
+
matrix[i - 1][j] + 1
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return matrix[str2.length][str1.length];
|
|
154
|
+
}
|