dotenv-diff 2.1.7 → 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 CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  ![Demo](./public/demo.gif)
4
4
 
5
- Easily compare your .env, .env.example, and other environment files (like .env.local, .env.production) to detect missing, extra, empty, or mismatched variables — and ensure they’re properly ignored by Git.
5
+ Easily scan your codebase to detect which environment variables are actually used in your code — and which ones are not.
6
6
 
7
- Or scan your codebase to find out which environment variables are actually used in your code, and which ones are not.
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.
8
12
 
9
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.
10
14
 
@@ -30,12 +34,8 @@ pnpm add -g dotenv-diff
30
34
  ```bash
31
35
  dotenv-diff
32
36
  ```
33
- ## What it checks
34
- dotenv-diff will automatically compare all matching .env* files in your project against .env.example, including:
35
- - `.env`
36
- - `.env.local`
37
- - `.env.production`
38
- - Any other .env.* file
37
+
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
39
 
40
40
  ## Why dotenv-diff?
41
41
 
@@ -45,12 +45,6 @@ dotenv-diff will automatically compare all matching .env* files in your project
45
45
  - **Enhance security**: Ensure sensitive variables are not accidentally committed to version control.
46
46
  - **Scale confidently**: Perfect for turbo monorepos and multi-environment setups.
47
47
 
48
- ## Scan your codebase for environment variable usage
49
-
50
- ```bash
51
- dotenv-diff --scan-usage
52
- ```
53
- This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file(s).
54
48
 
55
49
  ## CI/CD integration with `--ci` option
56
50
  You can scan and compare against specific environment file, eg. `.env.example`
@@ -64,26 +58,26 @@ And the `--example` option allows you to specify which `.env.example` file to co
64
58
 
65
59
  ```yaml
66
60
  - name: Check environment variables
67
- run: dotenv-diff --ci --scan-usage --example .env.example --ignore VITE_MODE,NODE_ENV
61
+ run: dotenv-diff --ci --example .env.example --ignore VITE_MODE,NODE_ENV
68
62
  ```
69
63
 
70
64
  You can also change the comparison file by using the `--example` flag to point to a different `.env.example` file.
71
65
 
72
66
  ```bash
73
- dotenv-diff --scan-usage --example .env.example.staging --ci
67
+ dotenv-diff --example .env.example.staging --ci
74
68
  ```
75
69
 
76
70
  ## Use it in a Turborepo Monorepo
77
71
 
78
72
  In a monorepo setup (e.g. with Turborepo), you often have multiple apps under apps/ and shared packages under packages/.
79
- You can run dotenv-diff from one app and still include files from sibling apps or packages.
73
+ You can run dotenv-diff from one app and still include files from your packages folder.
80
74
 
81
75
  For example, if you want to scan from the apps/app1 folder and also include code in packages/auth, you can do:
82
76
 
83
77
  ```json
84
78
  {
85
79
  "scripts": {
86
- "dotenv-diff": "dotenv-diff --scan-usage --example .env.example --include-files '../../packages/**/*' --ignore VITE_MODE"
80
+ "dotenv-diff": "dotenv-diff --example .env.example --include-files '../../packages/**/*' --ignore VITE_MODE"
87
81
  }
88
82
  }
89
83
  ```
@@ -91,65 +85,49 @@ For example, if you want to scan from the apps/app1 folder and also include code
91
85
  This will:
92
86
  - Compare the variables used in your `apps/app1` code against `apps/app1/.env.example`.
93
87
  - Also scan files in `../../packages`(like `packages/components/src/..`)
94
- - 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).
95
89
 
96
90
  ## Automatic fixes with `--fix`
97
91
 
98
- Use the `--fix` flag to automatically fix missing keys in your `.env` and remove duplicate keys.
92
+ Use the `--fix` flag to automatically fix missing keys in your `.env`.
99
93
 
100
94
  ```bash
101
95
  dotenv-diff --fix
102
96
  ```
103
97
 
104
- This will:
105
- - Add any missing keys from `.env.example` to your `.env` file with empty values
106
- - Remove duplicate keys in your `.env` file (keeping the last occurrence)
107
-
108
- ## Use --fix with scan-usage
109
-
110
- 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:
111
-
112
- ```bash
113
- dotenv-diff --scan-usage --fix
114
- ```
115
-
116
- Using `--fix`with `--scan-usage` will not detect duplicate keys, it will only add missing keys.
117
-
118
98
  ### Example workflow
119
99
 
120
- 1. You add `process.env.NEW_API_KEY` in your code.
121
- 2. You run `dotenv-diff --scan-usage --fix`.
100
+ 1. You have `process.env.NEW_API_KEY` in your code.
101
+ 2. You run `dotenv-diff --fix`.
122
102
  3. The tool automatically adds `NEW_API_KEY=` to your `.env` file.
123
103
 
124
- ## Scan your codebase for environment variables and add it directly to .env.example?
104
+ ## Show unused variables
105
+
106
+ As default, `dotenv-diff` will list variables that are defined in `.env` but never used in your codebase.
125
107
 
108
+ To disable this behavior, use the `--no-show-unused` flag:
126
109
  ```bash
127
- dotenv-diff --scan-usage --example .env.example --fix
110
+ dotenv-diff --no-show-unused
128
111
  ```
112
+ This will prevent the tool from listing variables that are defined in `.env` but not used in your codebase.
129
113
 
130
- This scans your codebase for environment variable usage and adds any missing keys directly to your `.env.example` file with empty values.
131
-
132
- ## Show unused variables
114
+ ## Show statistics
133
115
 
134
- Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
135
- ```bash
136
- dotenv-diff --scan-usage --show-unused
137
- ```
138
- 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.
139
117
 
140
- ## Show scan statistics
118
+ To disable this behavior, use the `--no-show-stats` flag:
141
119
 
142
120
  ```bash
143
- dotenv-diff --show-stats
121
+ dotenv-diff --no-show-stats
144
122
  ```
145
- This will display statistics about the scan, such as the number of files scanned, variables found, and any unused variables. It provides a quick overview of your environment variable usage.
123
+ This will prevent the tool from displaying statistics about the scan.
146
124
 
147
125
  ## include or exclude specific files for scanning
148
126
 
149
127
  You can specify which files to include or exclude from the scan using the `--include-files` and `--exclude-files` options:
150
128
 
151
129
  ```bash
152
- dotenv-diff --scan-usage --include-files '**/*.js,**/*.ts' --exclude-files '**/*.spec.ts'
130
+ dotenv-diff --include-files '**/*.js,**/*.ts' --exclude-files '**/*.spec.ts'
153
131
  ```
154
132
 
155
133
  By default, the scanner looks at JavaScript, TypeScript, Vue, and Svelte files.
@@ -157,32 +135,18 @@ The --include-files and --exclude-files options let you refine this list to focu
157
135
 
158
136
  ### Override with `--files`
159
137
 
160
- If you want to completely override the default include/exclude logic (for example, to also include test files), you can use --files:
138
+ If you want to completely override the default include/exclude logic (for example, to only include specific files), you can use --files:
161
139
  ```bash
162
- dotenv-diff --scan-usage --files '**/*.js'
140
+ dotenv-diff --files '**/*.js'
163
141
  ```
164
- This will only scan the files matching the given patterns, even if they would normally be excluded.
165
142
 
166
143
  ## Optional: Check values too
167
144
 
168
145
  ```bash
169
- dotenv-diff --check-values
146
+ dotenv-diff --check-values --compare
170
147
  ```
171
148
 
172
- When using the `--check-values` option, 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.
173
-
174
- ## Duplicate key warnings
175
-
176
- `dotenv-diff` warns when a `.env*` file contains the same key multiple times. The last occurrence wins. Suppress these warnings with `--allow-duplicates`.
177
-
178
- ## Only show specific categories
179
-
180
- Use the `--only` flag to restrict the comparison to specific categories. For example:
181
-
182
- ```bash
183
- dotenv-diff --only missing,extra
184
- ```
185
- 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.
186
150
 
187
151
  ## Ignore specific keys
188
152
 
@@ -216,35 +180,40 @@ dotenv-diff --no-color
216
180
 
217
181
  ## Compare specific files
218
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
+
219
189
  Override the autoscan and compare exactly two files:
220
190
 
221
191
  ```bash
222
- dotenv-diff --env .env.staging --example .env.example.staging
192
+ dotenv-diff --compare --env .env.local --example .env.example.local
223
193
  ```
224
194
 
225
195
  You can also fix only one side. For example, force a particular `.env` file and let the tool find the matching `.env.example`:
226
196
 
227
- ```bash
228
- dotenv-diff --env .env.production
229
- ```
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`.
230
198
 
231
- Or provide just an example file and let the tool locate the appropriate `.env`:
199
+ Use the `--only` flag to restrict the comparison to specific categories. For example:
232
200
 
233
201
  ```bash
234
- dotenv-diff --example .env.example.production
202
+ dotenv-diff --only missing,extra
235
203
  ```
204
+ This will only show missing and extra keys, ignoring empty, mismatched, duplicate keys and so on.
236
205
 
237
206
  ## Automatically create missing files
238
207
 
239
208
  Run non-interactively in CI environments with:
240
209
 
241
210
  ```bash
242
- dotenv-diff --yes # auto-create missing files without prompts
211
+ dotenv-diff --compare --yes # auto-create missing files without prompts
243
212
  ```
244
213
 
245
214
  You can also use `-y` as a shorthand for `--yes`.
246
215
 
247
- ## Automatic file creation prompts
216
+ ### Automatic file creation prompts
248
217
 
249
218
  If one of the files is missing, `dotenv-diff` will ask if you want to create it from the other:
250
219
 
@@ -253,7 +222,7 @@ If one of the files is missing, `dotenv-diff` will ask if you want to create it
253
222
 
254
223
  This makes it quick to set up environment files without manually copying or retyping variable names.
255
224
 
256
- ## Also checks your .gitignore
225
+ ### Also checks your .gitignore
257
226
 
258
227
  `dotenv-diff` will warn you if your `.env` file is **not** ignored by Git.
259
228
  This helps prevent accidentally committing sensitive environment variables.
@@ -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('--include-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to ADD to default scan patterns (extends default)')
20
- .option('--files <patterns>', '[requires --scan-usage] Comma-separated file patterns to scan (completely replaces default patterns)')
21
- .option('--exclude-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to exclude from scan')
22
- .option('--show-unused', '[requires --scan-usage] Show variables defined in .env but not used in code')
23
- .option('--show-stats', 'Show statistics (in scan-usage mode: scan stats, in compare mode: env compare stats)');
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
  }
@@ -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
- if (opts.scanUsage) {
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);
@@ -50,9 +50,6 @@ export async function compareMany(pairs, opts) {
50
50
  },
51
51
  });
52
52
  }
53
- else {
54
- // still call to keep previous hints? No—masked by --only.
55
- }
56
53
  // Duplicate detection (skip entirely if --only excludes it)
57
54
  let dupsEnv = [];
58
55
  let dupsEx = [];
@@ -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.bold('📊 Scan Statistics:'));
202
- console.log(chalk.gray(` Files scanned: ${scanResult.stats.filesScanned}`));
203
- console.log(chalk.gray(` Total usages found: ${scanResult.stats.totalUsages}`));
204
- console.log(chalk.gray(` Unique variables: ${scanResult.stats.uniqueVariables}`));
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.green(`✅ Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
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.gray(` Used in: ${usage.file}:${usage.line} (${usage.pattern})`));
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 scanUsage = Boolean(raw.scanUsage);
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 = Boolean(raw.showUnused);
33
- const showStats = Boolean(raw.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,8 @@
1
+ export type SecretFinding = {
2
+ file: string;
3
+ line: number;
4
+ kind: 'pattern' | 'entropy';
5
+ message: string;
6
+ snippet: string;
7
+ };
8
+ export declare function detectSecretsInSource(file: string, source: string): SecretFinding[];
@@ -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,4 +1,6 @@
1
1
  export { parseEnvFile } from './lib/parseEnv.js';
2
2
  export { diffEnv } from './lib/diffEnv.js';
3
- process.env.API_KEY = '12345'; // for testing purposes
4
- process.env.NEW_API_KEY = '67890'; // for testing purposes
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.1.7",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
- "description": "Find differences between .env and .env.example / .env.* files. And optionally scan your codebase to find environment variables in use.",
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
  },