dotenv-diff 2.1.6 → 2.2.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/README.md +56 -77
- package/dist/src/cli/program.js +7 -5
- package/dist/src/cli/run.js +3 -2
- package/dist/src/commands/compare.js +0 -3
- package/dist/src/commands/scanUsage.d.ts +6 -0
- package/dist/src/commands/scanUsage.js +34 -10
- package/dist/src/config/options.js +9 -3
- package/dist/src/config/types.d.ts +5 -0
- package/dist/src/core/entropy.d.ts +1 -0
- package/dist/src/core/entropy.js +16 -0
- package/dist/src/core/secretDetectors.d.ts +8 -0
- package/dist/src/core/secretDetectors.js +75 -0
- package/dist/src/index.js +4 -0
- package/dist/src/services/codeBaseScanner.d.ts +3 -0
- package/dist/src/services/codeBaseScanner.js +15 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# dotenv-diff
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Easily scan your codebase to detect which environment variables are actually used in your code — and which ones are not.
|
|
6
|
+
|
|
7
|
+
This package will:
|
|
8
|
+
- Scan your codebase to find environment variables in use.
|
|
9
|
+
- Show statistics about the usage of these variables.
|
|
10
|
+
- Detect missing environment variables.
|
|
11
|
+
- Find potential secrets in your codebase.
|
|
6
12
|
|
|
7
13
|
Optimized for JavaScript/TypeScript projects and frontend frameworks including Node.js, Next.js, Vite, SvelteKit, Nuxt, Vue, and Deno. Can also be used with other project types for basic .env file comparison.
|
|
8
14
|
|
|
@@ -28,19 +34,17 @@ pnpm add -g dotenv-diff
|
|
|
28
34
|
```bash
|
|
29
35
|
dotenv-diff
|
|
30
36
|
```
|
|
31
|
-
## What it checks
|
|
32
|
-
dotenv-diff will automatically compare all matching .env* files in your project against .env.example, including:
|
|
33
|
-
- `.env`
|
|
34
|
-
- `.env.local`
|
|
35
|
-
- `.env.production`
|
|
36
|
-
- Any other .env.* file
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file.
|
|
39
|
+
|
|
40
|
+
## Why dotenv-diff?
|
|
41
|
+
|
|
42
|
+
- **Prevent production issues**: Ensure all required environment variables are defined before deploying.
|
|
43
|
+
- **Avoid runtime errors**: Catch missing or misconfigured variables early in development.
|
|
44
|
+
- **Improve collaboration**: Keep your team aligned on necessary environment variables.
|
|
45
|
+
- **Enhance security**: Ensure sensitive variables are not accidentally committed to version control.
|
|
46
|
+
- **Scale confidently**: Perfect for turbo monorepos and multi-environment setups.
|
|
39
47
|
|
|
40
|
-
```bash
|
|
41
|
-
dotenv-diff --scan-usage
|
|
42
|
-
```
|
|
43
|
-
This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file(s).
|
|
44
48
|
|
|
45
49
|
## CI/CD integration with `--ci` option
|
|
46
50
|
You can scan and compare against specific environment file, eg. `.env.example`
|
|
@@ -50,30 +54,30 @@ Use the `--ci` flag for automated environments. This enables strict mode where t
|
|
|
50
54
|
|
|
51
55
|
And the `--example` option allows you to specify which `.env.example` file to compare against.
|
|
52
56
|
|
|
53
|
-
### Use it in Github Actions
|
|
57
|
+
### Use it in Github Actions Example:
|
|
54
58
|
|
|
55
59
|
```yaml
|
|
56
60
|
- name: Check environment variables
|
|
57
|
-
run: dotenv-diff --ci --
|
|
61
|
+
run: dotenv-diff --ci --example .env.example --ignore VITE_MODE,NODE_ENV
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
You can also change the comparison file by using the `--example` flag to point to a different `.env.example` file.
|
|
61
65
|
|
|
62
66
|
```bash
|
|
63
|
-
dotenv-diff --
|
|
67
|
+
dotenv-diff --example .env.example.staging --ci
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
## Use it in a Turborepo Monorepo
|
|
67
71
|
|
|
68
72
|
In a monorepo setup (e.g. with Turborepo), you often have multiple apps under apps/ and shared packages under packages/.
|
|
69
|
-
You can run dotenv-diff from one app and still include files from
|
|
73
|
+
You can run dotenv-diff from one app and still include files from your packages folder.
|
|
70
74
|
|
|
71
75
|
For example, if you want to scan from the apps/app1 folder and also include code in packages/auth, you can do:
|
|
72
76
|
|
|
73
77
|
```json
|
|
74
78
|
{
|
|
75
79
|
"scripts": {
|
|
76
|
-
"dotenv-diff": "dotenv-diff --
|
|
80
|
+
"dotenv-diff": "dotenv-diff --example .env.example --include-files '../../packages/**/*' --ignore VITE_MODE"
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
```
|
|
@@ -81,65 +85,49 @@ For example, if you want to scan from the apps/app1 folder and also include code
|
|
|
81
85
|
This will:
|
|
82
86
|
- Compare the variables used in your `apps/app1` code against `apps/app1/.env.example`.
|
|
83
87
|
- Also scan files in `../../packages`(like `packages/components/src/..`)
|
|
84
|
-
- Ignore variables like VITE_MODE that you only use in special cases.
|
|
88
|
+
- Ignore variables like VITE_MODE that you only use in special cases (.env.test).
|
|
85
89
|
|
|
86
90
|
## Automatic fixes with `--fix`
|
|
87
91
|
|
|
88
|
-
Use the `--fix` flag to automatically fix missing keys in your `.env
|
|
92
|
+
Use the `--fix` flag to automatically fix missing keys in your `.env`.
|
|
89
93
|
|
|
90
94
|
```bash
|
|
91
95
|
dotenv-diff --fix
|
|
92
96
|
```
|
|
93
97
|
|
|
94
|
-
This will:
|
|
95
|
-
- Add any missing keys from `.env.example` to your `.env` file with empty values
|
|
96
|
-
- Remove duplicate keys in your `.env` file (keeping the last occurrence)
|
|
97
|
-
|
|
98
|
-
## Use --fix with scan-usage
|
|
99
|
-
|
|
100
|
-
You can also combine `--fix` with `--scan-usage` to automatically add any missing keys that are used in your code but not defined in your `.env` file:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
dotenv-diff --scan-usage --fix
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Using `--fix`with `--scan-usage` will not detect duplicate keys, it will only add missing keys.
|
|
107
|
-
|
|
108
98
|
### Example workflow
|
|
109
99
|
|
|
110
|
-
1. You
|
|
111
|
-
2. You run `dotenv-diff --
|
|
100
|
+
1. You have `process.env.NEW_API_KEY` in your code.
|
|
101
|
+
2. You run `dotenv-diff --fix`.
|
|
112
102
|
3. The tool automatically adds `NEW_API_KEY=` to your `.env` file.
|
|
113
103
|
|
|
114
|
-
##
|
|
104
|
+
## Show unused variables
|
|
105
|
+
|
|
106
|
+
As default, `dotenv-diff` will list variables that are defined in `.env` but never used in your codebase.
|
|
115
107
|
|
|
108
|
+
To disable this behavior, use the `--no-show-unused` flag:
|
|
116
109
|
```bash
|
|
117
|
-
dotenv-diff --
|
|
110
|
+
dotenv-diff --no-show-unused
|
|
118
111
|
```
|
|
112
|
+
This will prevent the tool from listing variables that are defined in `.env` but not used in your codebase.
|
|
119
113
|
|
|
120
|
-
|
|
114
|
+
## Show statistics
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
|
|
125
|
-
```bash
|
|
126
|
-
dotenv-diff --scan-usage --show-unused
|
|
127
|
-
```
|
|
128
|
-
This will show you which variables are defined in your `.env` file but not used in your codebase. This helps you clean up unnecessary environment variables.
|
|
116
|
+
By default, `dotenv-diff` will show statistics about the scan, such as the number of files scanned, variables found, and unique variables used.
|
|
129
117
|
|
|
130
|
-
|
|
118
|
+
To disable this behavior, use the `--no-show-stats` flag:
|
|
131
119
|
|
|
132
120
|
```bash
|
|
133
|
-
dotenv-diff --show-stats
|
|
121
|
+
dotenv-diff --no-show-stats
|
|
134
122
|
```
|
|
135
|
-
This will
|
|
123
|
+
This will prevent the tool from displaying statistics about the scan.
|
|
136
124
|
|
|
137
125
|
## include or exclude specific files for scanning
|
|
138
126
|
|
|
139
127
|
You can specify which files to include or exclude from the scan using the `--include-files` and `--exclude-files` options:
|
|
140
128
|
|
|
141
129
|
```bash
|
|
142
|
-
dotenv-diff --
|
|
130
|
+
dotenv-diff --include-files '**/*.js,**/*.ts' --exclude-files '**/*.spec.ts'
|
|
143
131
|
```
|
|
144
132
|
|
|
145
133
|
By default, the scanner looks at JavaScript, TypeScript, Vue, and Svelte files.
|
|
@@ -147,32 +135,18 @@ The --include-files and --exclude-files options let you refine this list to focu
|
|
|
147
135
|
|
|
148
136
|
### Override with `--files`
|
|
149
137
|
|
|
150
|
-
If you want to completely override the default include/exclude logic (for example, to
|
|
138
|
+
If you want to completely override the default include/exclude logic (for example, to only include specific files), you can use --files:
|
|
151
139
|
```bash
|
|
152
|
-
dotenv-diff --
|
|
140
|
+
dotenv-diff --files '**/*.js'
|
|
153
141
|
```
|
|
154
|
-
This will only scan the files matching the given patterns, even if they would normally be excluded.
|
|
155
142
|
|
|
156
143
|
## Optional: Check values too
|
|
157
144
|
|
|
158
145
|
```bash
|
|
159
|
-
dotenv-diff --check-values
|
|
146
|
+
dotenv-diff --check-values --compare
|
|
160
147
|
```
|
|
161
148
|
|
|
162
|
-
When using the `--check-values` option
|
|
163
|
-
|
|
164
|
-
## Duplicate key warnings
|
|
165
|
-
|
|
166
|
-
`dotenv-diff` warns when a `.env*` file contains the same key multiple times. The last occurrence wins. Suppress these warnings with `--allow-duplicates`.
|
|
167
|
-
|
|
168
|
-
## Only show specific categories
|
|
169
|
-
|
|
170
|
-
Use the `--only` flag to restrict the comparison to specific categories. For example:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
dotenv-diff --only missing,extra
|
|
174
|
-
```
|
|
175
|
-
This will only show missing and extra keys, ignoring empty, mismatched, duplicate keys and so on.
|
|
149
|
+
When using the `--check-values` option together with `--compare`, the tool will also compare the actual values of the variables in `.env` and `.env.example`. It will report any mismatches found and it also compares values if .env.example defines a non-empty expected value.
|
|
176
150
|
|
|
177
151
|
## Ignore specific keys
|
|
178
152
|
|
|
@@ -206,35 +180,40 @@ dotenv-diff --no-color
|
|
|
206
180
|
|
|
207
181
|
## Compare specific files
|
|
208
182
|
|
|
183
|
+
The `--compare` flag will compare all matching .env* files in your project against .env.example, including:
|
|
184
|
+
- `.env`
|
|
185
|
+
- `.env.local`
|
|
186
|
+
- `.env.production`
|
|
187
|
+
- Any other .env.* file
|
|
188
|
+
|
|
209
189
|
Override the autoscan and compare exactly two files:
|
|
210
190
|
|
|
211
191
|
```bash
|
|
212
|
-
dotenv-diff --env .env.
|
|
192
|
+
dotenv-diff --compare --env .env.local --example .env.example.local
|
|
213
193
|
```
|
|
214
194
|
|
|
215
195
|
You can also fix only one side. For example, force a particular `.env` file and let the tool find the matching `.env.example`:
|
|
216
196
|
|
|
217
|
-
|
|
218
|
-
dotenv-diff --env .env.production
|
|
219
|
-
```
|
|
197
|
+
When using the `--compare` flag, `dotenv-diff` warns when a `.env*` file contains the same key multiple times. The last occurrence wins. Suppress these warnings with `--allow-duplicates`.
|
|
220
198
|
|
|
221
|
-
|
|
199
|
+
Use the `--only` flag to restrict the comparison to specific categories. For example:
|
|
222
200
|
|
|
223
201
|
```bash
|
|
224
|
-
dotenv-diff --
|
|
202
|
+
dotenv-diff --only missing,extra
|
|
225
203
|
```
|
|
204
|
+
This will only show missing and extra keys, ignoring empty, mismatched, duplicate keys and so on.
|
|
226
205
|
|
|
227
206
|
## Automatically create missing files
|
|
228
207
|
|
|
229
208
|
Run non-interactively in CI environments with:
|
|
230
209
|
|
|
231
210
|
```bash
|
|
232
|
-
dotenv-diff --yes # auto-create missing files without prompts
|
|
211
|
+
dotenv-diff --compare --yes # auto-create missing files without prompts
|
|
233
212
|
```
|
|
234
213
|
|
|
235
214
|
You can also use `-y` as a shorthand for `--yes`.
|
|
236
215
|
|
|
237
|
-
|
|
216
|
+
### Automatic file creation prompts
|
|
238
217
|
|
|
239
218
|
If one of the files is missing, `dotenv-diff` will ask if you want to create it from the other:
|
|
240
219
|
|
|
@@ -243,7 +222,7 @@ If one of the files is missing, `dotenv-diff` will ask if you want to create it
|
|
|
243
222
|
|
|
244
223
|
This makes it quick to set up environment files without manually copying or retyping variable names.
|
|
245
224
|
|
|
246
|
-
|
|
225
|
+
### Also checks your .gitignore
|
|
247
226
|
|
|
248
227
|
`dotenv-diff` will warn you if your `.env` file is **not** ignored by Git.
|
|
249
228
|
This helps prevent accidentally committing sensitive environment variables.
|
package/dist/src/cli/program.js
CHANGED
|
@@ -16,9 +16,11 @@ export function createProgram() {
|
|
|
16
16
|
.option('--no-color', 'Disable colored output')
|
|
17
17
|
.option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)')
|
|
18
18
|
.option('--scan-usage', 'Scan codebase for environment variable usage')
|
|
19
|
-
.option('--
|
|
20
|
-
.option('--files <patterns>', '
|
|
21
|
-
.option('--
|
|
22
|
-
.option('--
|
|
23
|
-
.option('--show-
|
|
19
|
+
.option('--compare', 'Compare .env and .env.example files')
|
|
20
|
+
.option('--include-files <patterns>', 'Comma-separated file patterns to ADD to default scan patterns (extends default)')
|
|
21
|
+
.option('--files <patterns>', 'Comma-separated file patterns to scan (completely replaces default patterns)')
|
|
22
|
+
.option('--exclude-files <patterns>', 'Comma-separated file patterns to exclude from scan')
|
|
23
|
+
.option('--no-show-unused', 'Do not list variables that are defined in .env but not used in code')
|
|
24
|
+
.option('--no-show-stats', 'Do not show statistics')
|
|
25
|
+
.option('--no-secrets', 'Disable secret detection during scan (enabled by default)');
|
|
24
26
|
}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -14,7 +14,8 @@ export async function run(program) {
|
|
|
14
14
|
if (opts.noColor) {
|
|
15
15
|
chalk.level = 0; // disable colors globally
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// DEFAULT: scan-usage unless --compare is set
|
|
18
|
+
if (!opts.compare) {
|
|
18
19
|
const envPath = opts.envFlag || (fs.existsSync('.env') ? '.env' : undefined);
|
|
19
20
|
const { exitWithError } = await scanUsage({
|
|
20
21
|
cwd: opts.cwd,
|
|
@@ -30,6 +31,7 @@ export async function run(program) {
|
|
|
30
31
|
showStats: opts.showStats,
|
|
31
32
|
isCiMode: opts.isCiMode,
|
|
32
33
|
files: opts.files,
|
|
34
|
+
secrets: opts.secrets,
|
|
33
35
|
});
|
|
34
36
|
process.exit(exitWithError ? 1 : 0);
|
|
35
37
|
}
|
|
@@ -105,7 +107,6 @@ export async function run(program) {
|
|
|
105
107
|
isCiMode: opts.isCiMode,
|
|
106
108
|
});
|
|
107
109
|
if (res.shouldExit) {
|
|
108
|
-
// For JSON mode, emit an empty report to keep output machine-friendly (optional; safe).
|
|
109
110
|
if (opts.json)
|
|
110
111
|
console.log(JSON.stringify([], null, 2));
|
|
111
112
|
process.exit(res.exitCode);
|
|
@@ -34,6 +34,12 @@ export interface ScanJsonEntry {
|
|
|
34
34
|
}>;
|
|
35
35
|
comparedAgainst?: string;
|
|
36
36
|
totalEnvVariables?: number;
|
|
37
|
+
secrets?: Array<{
|
|
38
|
+
file: string;
|
|
39
|
+
line: number;
|
|
40
|
+
message: string;
|
|
41
|
+
snippet: string;
|
|
42
|
+
}>;
|
|
37
43
|
}
|
|
38
44
|
/**
|
|
39
45
|
* Scans codebase for environment variable usage and compares with .env file
|
|
@@ -125,6 +125,10 @@ export async function scanUsage(opts) {
|
|
|
125
125
|
console.log(chalk.green('✅ Auto-fix applied: no changes needed.'));
|
|
126
126
|
console.log();
|
|
127
127
|
}
|
|
128
|
+
if (scanResult.missing.length > 0 && !opts.json && !opts.fix) {
|
|
129
|
+
console.log(chalk.gray('💡 Tip: Run with `--fix` to add missing keys'));
|
|
130
|
+
console.log();
|
|
131
|
+
}
|
|
128
132
|
return result;
|
|
129
133
|
}
|
|
130
134
|
/**
|
|
@@ -172,6 +176,14 @@ function createJsonOutput(scanResult, opts, comparedAgainst, totalEnvVariables)
|
|
|
172
176
|
missing: missingGrouped,
|
|
173
177
|
unused: scanResult.unused,
|
|
174
178
|
};
|
|
179
|
+
if (scanResult.secrets?.length) {
|
|
180
|
+
output.secrets = scanResult.secrets.map((s) => ({
|
|
181
|
+
file: s.file,
|
|
182
|
+
line: s.line,
|
|
183
|
+
message: s.message,
|
|
184
|
+
snippet: s.snippet,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
175
187
|
// Add comparison info if we compared against a file
|
|
176
188
|
if (comparedAgainst) {
|
|
177
189
|
output.comparedAgainst = comparedAgainst;
|
|
@@ -198,15 +210,15 @@ function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
|
198
210
|
}
|
|
199
211
|
// Show stats if requested
|
|
200
212
|
if (opts.showStats) {
|
|
201
|
-
console.log(chalk.
|
|
202
|
-
console.log(chalk.
|
|
203
|
-
console.log(chalk.
|
|
204
|
-
console.log(chalk.
|
|
213
|
+
console.log(chalk.blue('📊 Scan Statistics:'));
|
|
214
|
+
console.log(chalk.blue.dim(` Files scanned: ${scanResult.stats.filesScanned}`));
|
|
215
|
+
console.log(chalk.blue.dim(` Total usages found: ${scanResult.stats.totalUsages}`));
|
|
216
|
+
console.log(chalk.blue.dim(` Unique variables: ${scanResult.stats.uniqueVariables}`));
|
|
205
217
|
console.log();
|
|
206
218
|
}
|
|
207
219
|
// Always show found variables when not comparing or when no missing variables
|
|
208
220
|
if (!comparedAgainst || scanResult.missing.length === 0) {
|
|
209
|
-
console.log(chalk.
|
|
221
|
+
console.log(chalk.blue(`🌐 Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
|
|
210
222
|
console.log();
|
|
211
223
|
// List all variables found (if any)
|
|
212
224
|
if (scanResult.stats.uniqueVariables > 0) {
|
|
@@ -225,7 +237,7 @@ function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
|
225
237
|
if (opts.showStats) {
|
|
226
238
|
const displayUsages = usages.slice(0, 2);
|
|
227
239
|
displayUsages.forEach((usage) => {
|
|
228
|
-
console.log(chalk.
|
|
240
|
+
console.log(chalk.blue.dim(` Used in: ${usage.file}:${usage.line} (${usage.pattern})`));
|
|
229
241
|
});
|
|
230
242
|
if (usages.length > 2) {
|
|
231
243
|
console.log(chalk.gray(` ... and ${usages.length - 2} more locations`));
|
|
@@ -273,6 +285,22 @@ function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
|
273
285
|
});
|
|
274
286
|
console.log();
|
|
275
287
|
}
|
|
288
|
+
if (scanResult.secrets && scanResult.secrets.length > 0) {
|
|
289
|
+
console.log(chalk.yellow('🔒 Potential secrets detected in codebase:'));
|
|
290
|
+
const byFile = new Map();
|
|
291
|
+
for (const f of scanResult.secrets) {
|
|
292
|
+
if (!byFile.has(f.file))
|
|
293
|
+
byFile.set(f.file, []);
|
|
294
|
+
byFile.get(f.file).push(f);
|
|
295
|
+
}
|
|
296
|
+
for (const [file, findings] of byFile) {
|
|
297
|
+
console.log(chalk.bold(` ${file}`));
|
|
298
|
+
for (const f of findings) {
|
|
299
|
+
console.log(chalk.yellow(` - Line ${f.line}: ${f.message}\n ${chalk.dim(f.snippet)}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
console.log();
|
|
303
|
+
}
|
|
276
304
|
// Success message for env file comparison
|
|
277
305
|
if (comparedAgainst && scanResult.missing.length === 0) {
|
|
278
306
|
console.log(chalk.green(`✅ All used environment variables are defined in ${comparedAgainst}`));
|
|
@@ -281,9 +309,5 @@ function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
|
281
309
|
}
|
|
282
310
|
console.log();
|
|
283
311
|
}
|
|
284
|
-
if (scanResult.missing.length > 0 && !opts.json && !opts.fix) {
|
|
285
|
-
console.log(chalk.gray('💡 Tip: Run with `--fix` to add these missing keys to your .env file automatically.'));
|
|
286
|
-
console.log();
|
|
287
|
-
}
|
|
288
312
|
return { exitWithError };
|
|
289
313
|
}
|
|
@@ -26,12 +26,15 @@ export function normalizeOptions(raw) {
|
|
|
26
26
|
const json = Boolean(raw.json);
|
|
27
27
|
const onlyParsed = parseCategories(raw.only, '--only');
|
|
28
28
|
const only = onlyParsed.length ? onlyParsed : undefined;
|
|
29
|
-
const
|
|
29
|
+
const noColor = Boolean(raw.noColor);
|
|
30
|
+
const compare = Boolean(raw.compare);
|
|
31
|
+
const scanUsage = raw.scanUsage ?? !compare;
|
|
30
32
|
const includeFiles = parseList(raw.includeFiles);
|
|
31
33
|
const excludeFiles = parseList(raw.excludeFiles);
|
|
32
|
-
const showUnused =
|
|
33
|
-
const showStats =
|
|
34
|
+
const showUnused = raw.showUnused === false ? false : true;
|
|
35
|
+
const showStats = raw.showStats === false ? false : true;
|
|
34
36
|
const files = parseList(raw.files);
|
|
37
|
+
const secrets = raw.secrets === false ? false : true;
|
|
35
38
|
const ignore = parseList(raw.ignore);
|
|
36
39
|
const ignoreRegex = [];
|
|
37
40
|
for (const pattern of parseList(raw.ignoreRegex)) {
|
|
@@ -62,11 +65,14 @@ export function normalizeOptions(raw) {
|
|
|
62
65
|
ignoreRegex,
|
|
63
66
|
cwd,
|
|
64
67
|
only,
|
|
68
|
+
compare,
|
|
69
|
+
noColor,
|
|
65
70
|
scanUsage,
|
|
66
71
|
includeFiles,
|
|
67
72
|
excludeFiles,
|
|
68
73
|
showUnused,
|
|
69
74
|
showStats,
|
|
70
75
|
files,
|
|
76
|
+
secrets,
|
|
71
77
|
};
|
|
72
78
|
}
|
|
@@ -13,6 +13,7 @@ export type Options = {
|
|
|
13
13
|
ignoreRegex: RegExp[];
|
|
14
14
|
cwd: string;
|
|
15
15
|
only?: Category[];
|
|
16
|
+
compare: boolean;
|
|
16
17
|
scanUsage: boolean;
|
|
17
18
|
includeFiles: string[];
|
|
18
19
|
excludeFiles: string[];
|
|
@@ -20,6 +21,7 @@ export type Options = {
|
|
|
20
21
|
showStats: boolean;
|
|
21
22
|
files?: string[];
|
|
22
23
|
noColor?: boolean;
|
|
24
|
+
secrets: boolean;
|
|
23
25
|
};
|
|
24
26
|
export type RawOptions = {
|
|
25
27
|
checkValues?: boolean;
|
|
@@ -33,12 +35,15 @@ export type RawOptions = {
|
|
|
33
35
|
ignore?: string | string[];
|
|
34
36
|
ignoreRegex?: string | string[];
|
|
35
37
|
only?: string | string[];
|
|
38
|
+
compare?: boolean;
|
|
39
|
+
noColor?: boolean;
|
|
36
40
|
scanUsage?: boolean;
|
|
37
41
|
includeFiles?: string | string[];
|
|
38
42
|
excludeFiles?: string | string[];
|
|
39
43
|
showUnused?: boolean;
|
|
40
44
|
showStats?: boolean;
|
|
41
45
|
files?: string | string[];
|
|
46
|
+
secrets?: boolean;
|
|
42
47
|
};
|
|
43
48
|
export type CompareJsonEntry = {
|
|
44
49
|
env: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function shannonEntropyNormalized(s: string): number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function shannonEntropyNormalized(s) {
|
|
2
|
+
if (!s)
|
|
3
|
+
return 0;
|
|
4
|
+
const freq = new Map();
|
|
5
|
+
for (const ch of s)
|
|
6
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
7
|
+
const len = s.length;
|
|
8
|
+
let H = 0;
|
|
9
|
+
for (const [, c] of freq) {
|
|
10
|
+
const p = c / len;
|
|
11
|
+
H += -p * Math.log2(p);
|
|
12
|
+
}
|
|
13
|
+
// Antag alfabet ~72 tegn (A-Za-z0-9+/_- mv.)
|
|
14
|
+
const maxH = Math.log2(72);
|
|
15
|
+
return Math.min(1, H / maxH);
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { shannonEntropyNormalized } from './entropy.js';
|
|
2
|
+
const SUSPICIOUS_KEYS = /\b(pass(word)?|secret|token|apikey|api_key|key|auth|bearer|private|client_secret|access[_-]?token)\b/i;
|
|
3
|
+
const PROVIDER_PATTERNS = [
|
|
4
|
+
/\bAKIA[0-9A-Z]{16}\b/, // AWS access key id
|
|
5
|
+
/\bASIA[0-9A-Z]{16}\b/, // AWS temp key
|
|
6
|
+
/\bghp_[0-9A-Za-z]{30,}\b/, // GitHub token
|
|
7
|
+
/\bsk_live_[0-9a-zA-Z]{24,}\b/, // Stripe live secret
|
|
8
|
+
/\bsk_test_[0-9a-zA-Z]{24,}\b/, // Stripe test secret
|
|
9
|
+
/\bAIza[0-9A-Za-z\-_]{20,}\b/, // Google API key
|
|
10
|
+
];
|
|
11
|
+
const LONG_LITERAL = /["'`]{1}([A-Za-z0-9+/_\-]{24,})["'`]{1}/g;
|
|
12
|
+
function looksHarmlessLiteral(s) {
|
|
13
|
+
return /^https?:\/\//i.test(s) || /\S+@\S+/.test(s);
|
|
14
|
+
}
|
|
15
|
+
// Undgå støj fra typiske test/fixture/mock filer (meget konservativ liste)
|
|
16
|
+
function isProbablyTestPath(p) {
|
|
17
|
+
return (/\b(__tests__|__mocks__|fixtures|sandbox|samples)\b/i.test(p) ||
|
|
18
|
+
/\.(spec|test)\.[jt]sx?$/.test(p));
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_SECRET_THRESHOLD = 0.9;
|
|
21
|
+
export function detectSecretsInSource(file, source) {
|
|
22
|
+
const threshold = isProbablyTestPath(file) ? 0.95 : DEFAULT_SECRET_THRESHOLD;
|
|
23
|
+
const findings = [];
|
|
24
|
+
const lines = source.split(/\r?\n/);
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const lineNo = i + 1;
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
// 1) Suspicious key literal assignments
|
|
29
|
+
if (SUSPICIOUS_KEYS.test(line)) {
|
|
30
|
+
const m = line.match(/=\s*["'`](.+?)["'`]/);
|
|
31
|
+
if (m && m[1] && !looksHarmlessLiteral(m[1]) && m[1].length >= 12) {
|
|
32
|
+
findings.push({
|
|
33
|
+
file,
|
|
34
|
+
line: lineNo,
|
|
35
|
+
kind: 'pattern',
|
|
36
|
+
message: 'matches password/secret/token-like literal assignment',
|
|
37
|
+
snippet: line.trim().slice(0, 180),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 2) Provider patterns
|
|
42
|
+
for (const rx of PROVIDER_PATTERNS) {
|
|
43
|
+
if (rx.test(line)) {
|
|
44
|
+
findings.push({
|
|
45
|
+
file,
|
|
46
|
+
line: lineNo,
|
|
47
|
+
kind: 'pattern',
|
|
48
|
+
message: 'matches known provider key pattern',
|
|
49
|
+
snippet: line.trim().slice(0, 180),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 3) High-entropy long literals
|
|
54
|
+
LONG_LITERAL.lastIndex = 0;
|
|
55
|
+
let lm;
|
|
56
|
+
while ((lm = LONG_LITERAL.exec(line))) {
|
|
57
|
+
const literal = lm[1];
|
|
58
|
+
if (looksHarmlessLiteral(literal))
|
|
59
|
+
continue;
|
|
60
|
+
if (literal.length < 32)
|
|
61
|
+
continue; // ekstra støjfilter
|
|
62
|
+
const ent = shannonEntropyNormalized(literal);
|
|
63
|
+
if (ent >= threshold) {
|
|
64
|
+
findings.push({
|
|
65
|
+
file,
|
|
66
|
+
line: lineNo,
|
|
67
|
+
kind: 'entropy',
|
|
68
|
+
message: `found high-entropy string (len ${literal.length}, H≈${ent.toFixed(2)})`,
|
|
69
|
+
snippet: line.trim().slice(0, 180),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { parseEnvFile } from './lib/parseEnv.js';
|
|
2
2
|
export { diffEnv } from './lib/diffEnv.js';
|
|
3
|
+
process.env.API_KEY = '1234asdasdasasdsdfdfaasddsa5';
|
|
4
|
+
process.env.NEW_API_KEY = '67890';
|
|
5
|
+
const password = '3fg400asipkfoemkfmojwpajwmdklaosjfiop';
|
|
6
|
+
const service = 'http://my-service.com';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SecretFinding } from '../core/secretDetectors.js';
|
|
1
2
|
export interface EnvUsage {
|
|
2
3
|
variable: string;
|
|
3
4
|
file: string;
|
|
@@ -13,6 +14,7 @@ export interface ScanOptions {
|
|
|
13
14
|
ignore: string[];
|
|
14
15
|
ignoreRegex: RegExp[];
|
|
15
16
|
files?: string[];
|
|
17
|
+
secrets?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface ScanResult {
|
|
18
20
|
used: EnvUsage[];
|
|
@@ -23,6 +25,7 @@ export interface ScanResult {
|
|
|
23
25
|
totalUsages: number;
|
|
24
26
|
uniqueVariables: number;
|
|
25
27
|
};
|
|
28
|
+
secrets: SecretFinding[];
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* Scans the codebase for environment variable usage based on the provided options.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fsSync from 'fs';
|
|
4
|
+
import { detectSecretsInSource, } from '../core/secretDetectors.js';
|
|
4
5
|
// Framework-specific patterns for finding environment variable usage
|
|
5
6
|
const ENV_PATTERNS = [
|
|
6
7
|
{
|
|
@@ -74,11 +75,24 @@ export async function scanCodebase(opts) {
|
|
|
74
75
|
});
|
|
75
76
|
const allUsages = [];
|
|
76
77
|
let filesScanned = 0;
|
|
78
|
+
const allSecrets = [];
|
|
77
79
|
for (const filePath of files) {
|
|
78
80
|
try {
|
|
79
81
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
80
82
|
const fileUsages = await scanFile(filePath, content, opts);
|
|
81
83
|
allUsages.push(...fileUsages);
|
|
84
|
+
if (opts.secrets) {
|
|
85
|
+
try {
|
|
86
|
+
// Brug relativ path i findings, så output matcher dine øvrige prints
|
|
87
|
+
const relativePath = path.relative(opts.cwd, filePath);
|
|
88
|
+
const sec = detectSecretsInSource(relativePath, content);
|
|
89
|
+
if (sec.length)
|
|
90
|
+
allSecrets.push(...sec);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// aldrig fail scannet pga. detector; bare fortsæt
|
|
94
|
+
}
|
|
95
|
+
}
|
|
82
96
|
filesScanned++;
|
|
83
97
|
}
|
|
84
98
|
catch {
|
|
@@ -94,6 +108,7 @@ export async function scanCodebase(opts) {
|
|
|
94
108
|
used: filteredUsages,
|
|
95
109
|
missing: [],
|
|
96
110
|
unused: [],
|
|
111
|
+
secrets: allSecrets,
|
|
97
112
|
stats: {
|
|
98
113
|
filesScanned,
|
|
99
114
|
totalUsages: filteredUsages.length,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "scan your codebase to find environment variables in use.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotenv-diff": "dist/bin/dotenv-diff.js"
|
|
8
8
|
},
|