Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ This project follows [Keep a Changelog](https://keepachangelog.com/) and [Semant
### Fixed
-

## [2.3.9] - 2025-12-09
### Added
- Added expiration date warnings for environment variables in codebase scanner.
- Added inconsistent naming warnings for environment variables in codebase scanner.

### Changed
- Changed health score calculation weights for better accuracy.
- Removed CSP detection from codebase scanner, as it was causing false positives in some cases for backend frameworks.

## [2.3.8] - 2025-12-08
### Added
- Added variables not using uppercase letters warning to codebase scanner.
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,27 @@ The health score is calculated based on several factors, including:
- Unused variables in your `.env` or `.env.example` file.
- Framework specific warning for SvelteKit or Next.js (depending on detected framework).

## Expiration date warnings

By default, `dotenv-diff` will detect environment variables with expiration dates and warn you if they are expired or about to expire.
To specify an expiration date for an environment variable, add a comment in the following format on the same line this an example of a .env.example file:

```bash
# @expire YYYY-MM-DD
API_TOKEN=
```

When you run `dotenv-diff`, it will check the expiration dates and display warnings for any variables that are expired or will expire soon.

## Inconsistent naming pattern warnings

By default `dotenv-diff` will detect environment variables that have inconsistent naming patterns, fx `APIKEY` & `API_KEY` will give you are warning to only use the `API_KEY`
To disable this behavior, use the `--no-inconsistent-naming-warnings` flags respectively, or set it to false in the config file:

```bash
"inconsistentNamingWarnings": false
```

## Show unused variables

As default, `dotenv-diff` will list variables that are defined in `.env` but never used in your codebase.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dotenv-diff",
"version": "2.3.8",
"version": "2.3.9",
"type": "module",
"description": "Scan your codebase to find environment variables in use.",
"bin": {
Expand Down
13 changes: 13 additions & 0 deletions src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,18 @@ export function createProgram() {
'Enable uppercase key validation (enabled by default)',
)
.option('--no-uppercase-keys', 'Disable uppercase key validation')
.option(
'--expire-warnings',
'Enable expiration date warnings for environment variables (enabled by default)',
)
.option('--no-expire-warnings', 'Disable expiration date warnings')
.option(
'--inconsistent-naming-warnings',
'Enable inconsistent naming pattern warnings (enabled by default)',
)
.option(
'--no-inconsistent-naming-warnings',
'Disable inconsistent naming pattern warnings',
)
.option('--init', 'Create a sample dotenv-diff.config.json file');
}
2 changes: 2 additions & 0 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ async function runScanMode(opts: Options): Promise<void> {
ignoreUrls: opts.ignoreUrls ?? [],
noCompare: opts.noCompare ?? false,
uppercaseKeys: opts.uppercaseKeys ?? true,
expireWarnings: opts.expireWarnings,
inconsistentNamingWarnings: opts.inconsistentNamingWarnings,
...(opts.files ? { files: opts.files } : {}),
});

Expand Down
16 changes: 10 additions & 6 deletions src/commands/scanUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { printComparisonError } from '../ui/scan/printComparisonError.js';
import { hasIgnoreComment } from '../core/secretDetectors.js';
import { frameworkValidator } from '../core/frameworkValidator.js';
import { detectSecretsInExample } from '../core/exampleSecretDetector.js';
import { detectUppercaseKeys } from '../core/detectUppercaseKeys.js';

/**
* Filters out commented usages from the list.
Expand Down Expand Up @@ -126,10 +125,6 @@ export async function scanUsage(
scanResult.frameworkWarnings = frameworkWarnings;
}

if (opts.uppercaseKeys) {
scanResult.uppercaseWarnings = detectUppercaseKeys(scanResult.used);
}

// Determine which file to compare against
const compareFile = determineComparisonFile(opts);
let envVariables: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -166,6 +161,13 @@ export async function scanUsage(
if (result.uppercaseWarnings) {
scanResult.uppercaseWarnings = result.uppercaseWarnings;
}
if (result.expireWarnings) {
scanResult.expireWarnings = result.expireWarnings;
}
if (result.inconsistentNamingWarnings) {
scanResult.inconsistentNamingWarnings =
result.inconsistentNamingWarnings;
}
if (result.exampleFull && result.comparedAgainst === '.env.example') {
scanResult.exampleWarnings = detectSecretsInExample(result.exampleFull);
}
Expand Down Expand Up @@ -207,7 +209,9 @@ export async function scanUsage(
(scanResult.exampleWarnings?.length ?? 0) > 0 ||
(scanResult.frameworkWarnings?.length ?? 0) > 0 ||
(scanResult.logged?.length ?? 0) > 0 ||
(scanResult.uppercaseWarnings?.length ?? 0) > 0
(scanResult.uppercaseWarnings?.length ?? 0) > 0 ||
(scanResult.expireWarnings?.length ?? 0) > 0 ||
(scanResult.inconsistentNamingWarnings?.length ?? 0) > 0
),
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export function normalizeOptions(raw: RawOptions): Options {

const ignoreUrls = parseList(raw.ignoreUrls);
const uppercaseKeys = raw.uppercaseKeys !== false;
const expireWarnings = raw.expireWarnings !== false;
const inconsistentNamingWarnings = raw.inconsistentNamingWarnings !== false;

const cwd = process.cwd();
const envFlag =
Expand Down Expand Up @@ -139,5 +141,7 @@ export function normalizeOptions(raw: RawOptions): Options {
ignoreUrls,
noCompare,
uppercaseKeys,
expireWarnings,
inconsistentNamingWarnings,
};
}
38 changes: 36 additions & 2 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type Options = {
ignoreUrls?: string[];
noCompare: boolean;
uppercaseKeys: boolean;
expireWarnings: boolean;
inconsistentNamingWarnings: boolean;
};

/** Type representing the raw options for the comparison
Expand Down Expand Up @@ -84,6 +86,8 @@ export type RawOptions = {
noCompare?: boolean;
init?: boolean;
uppercaseKeys?: boolean;
expireWarnings?: boolean;
inconsistentNamingWarnings?: boolean;
};

/**
Expand Down Expand Up @@ -153,11 +157,12 @@ export interface ScanResult {
env?: Array<{ key: string; count: number }>;
example?: Array<{ key: string; count: number }>;
};
hasCsp?: boolean;
frameworkWarnings?: frameworkWarning[];
exampleWarnings?: ExampleSecretWarning[];
logged: EnvUsage[];
uppercaseWarnings?: UppercaseWarning[];
expireWarnings?: ExpireWarning[];
inconsistentNamingWarnings?: InconsistentNamingWarning[];
}

/** Options for scanning the codebase for environment variable usage. */
Expand All @@ -173,6 +178,8 @@ export interface ScanUsageOptions extends ScanOptions {
allowDuplicates?: boolean;
strict?: boolean;
uppercaseKeys?: boolean;
expireWarnings?: boolean;
inconsistentNamingWarnings?: boolean;
}

export interface ScanJsonEntry {
Expand Down Expand Up @@ -212,13 +219,26 @@ export interface ScanJsonEntry {
env?: Array<{ key: string; count: number }>;
example?: Array<{ key: string; count: number }>;
};
hasCsp?: boolean;
logged?: Array<{
variable: string;
file: string;
line: number;
context: string;
}>;
expireWarnings?: Array<{
key: string;
date: string;
daysLeft: number;
}>;
uppercaseWarnings?: Array<{
key: string;
suggestion: string;
}>;
inconsistentNamingWarnings?: Array<{
key1: string;
key2: string;
suggestion: string;
}>;
}

// Type for grouped usages by variable
Expand All @@ -239,6 +259,8 @@ export interface ComparisonOptions {
showStats?: boolean;
strict?: boolean;
uppercaseKeys?: boolean;
expireWarnings?: boolean;
inconsistentNamingWarnings?: boolean;
}

export interface FilePair {
Expand Down Expand Up @@ -279,3 +301,15 @@ export interface UppercaseWarning {
key: string;
suggestion: string;
}

export interface ExpireWarning {
key: string;
date: string;
daysLeft: number;
}

export interface InconsistentNamingWarning {
key1: string;
key2: string;
suggestion: string;
}
13 changes: 8 additions & 5 deletions src/core/computeHealthScore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,30 @@ export function computeHealthScore(scan: ScanResult): number {
const highSecrets = scan.secrets?.filter((s) => s.severity === 'high') ?? [];
const medSecrets = scan.secrets?.filter((s) => s.severity === 'medium') ?? [];

score -= highSecrets.length * 15;
score -= medSecrets.length * 5;
score -= highSecrets.length * 20;
score -= medSecrets.length * 10;

// === 2. Missing environment variables ===
score -= scan.missing.length * 5;
score -= scan.missing.length * 20;

// === 3. Uppercase naming issues ===
score -= (scan.uppercaseWarnings?.length ?? 0) * 2;

// === 4. Console logging ===
score -= (scan.logged?.length ?? 0) * 5;
score -= (scan.logged?.length ?? 0) * 10;

// === 5. Unused vars (less important) ===
score -= (scan.unused?.length ?? 0) * 1;

// === 6. Framework warnings ===
score -= (scan.frameworkWarnings?.length ?? 0) * 2;
score -= (scan.frameworkWarnings?.length ?? 0) * 5;

// === 7. Example secrets ===
score -= (scan.exampleWarnings?.length ?? 0) * 10;

// === 8. Expiration warnings ===
score -= (scan.expireWarnings?.length ?? 0) * 5;

// Never go below 0 or above 100
return Math.max(0, Math.min(100, score));
}
40 changes: 0 additions & 40 deletions src/core/cspDetector.ts

This file was deleted.

57 changes: 57 additions & 0 deletions src/core/detectExpirations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fs from 'fs';
import type { ExpireWarning } from '../config/types.js';

/**
* Detects expiration warnings in a dotenv file.
* fx:
*
* # @expire 2024-12-31
* API_KEY=
*
* This will generate a warning that API_KEY expires on 2024-12-31.
* @param filePath - Path to the dotenv file
* @returns Array of expiration warnings
*/
export function detectExpirations(filePath: string): ExpireWarning[] {
const lines = fs.readFileSync(filePath, 'utf8').split('\n');

const warnings: ExpireWarning[] = [];

const reg = /(\/\/|#)?\s*@?expire\s+(\d{4}-\d{2}-\d{2})/i;

let pendingExpire: string | null = null;

for (const raw of lines) {
const line = raw.trim();

const expireMatch = line.match(reg);

if (expireMatch) {
pendingExpire = expireMatch[2] ?? null; // capture dato
continue;
}

const isEnvKey = /^[A-Za-z0-9_.-]+=/.test(line);

if (isEnvKey) {
const key = line.split('=')[0];

if (key && pendingExpire) {
const expireDate = new Date(pendingExpire);
const now = new Date();
const diffMs = expireDate.getTime() - now.getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));

warnings.push({
key,
date: pendingExpire,
daysLeft: diffDays,
});

pendingExpire = null;
}
}
}

return warnings;
}
Loading
Loading