diff --git a/packages/cli/package.json b/packages/cli/package.json index c97619d..935dcc1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,8 @@ "keywords": [ "dotenv", "env", + "environment", + "variables", "dotenv-diff", "env-check", "env-validate", diff --git a/packages/cli/src/services/processComparisonFile.ts b/packages/cli/src/services/processComparisonFile.ts index 9e37ad3..1c4d403 100644 --- a/packages/cli/src/services/processComparisonFile.ts +++ b/packages/cli/src/services/processComparisonFile.ts @@ -23,7 +23,7 @@ import type { /** * Result of processing comparison file */ -interface ProcessComparisonResult { +export interface ProcessComparisonResult { scanResult: ScanResult; envVariables: Record; comparedAgainst: string; diff --git a/packages/cli/test/unit/commands/scanUsage.test.ts b/packages/cli/test/unit/commands/scanUsage.test.ts index 4a4360d..afe09f2 100644 --- a/packages/cli/test/unit/commands/scanUsage.test.ts +++ b/packages/cli/test/unit/commands/scanUsage.test.ts @@ -5,6 +5,7 @@ import type { } from '../../../src/config/types.js'; import { type SecretFinding } from '../../../src/core/security/secretDetectors.js'; import { promptNoEnvScenario } from '../../../src/commands/prompts/promptNoEnvScenario.js'; +import { ProcessComparisonResult } from '../../../src/services/processComparisonFile.js'; vi.mock('../../../src/services/scanCodebase.js', () => ({ scanCodebase: vi.fn(), @@ -129,7 +130,7 @@ describe('scanUsage', () => { vi.mocked(processComparisonFile).mockReturnValue({ error: { message: 'err', shouldExit: true }, - } as any); + } as ProcessComparisonResult); vi.mocked(printComparisonError).mockReturnValue({ exit: true }); @@ -171,7 +172,7 @@ describe('scanUsage', () => { severity: 'medium', }, ], - } as any); + } as ScanResult); const result = await scanUsage({ ...baseOpts, json: true, strict: false }); @@ -230,13 +231,17 @@ describe('scanUsage', () => { vi.mocked(processComparisonFile).mockReturnValue({ scanResult: { ...baseScanResult }, comparedAgainst: '.env', + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], fix: { fixApplied: false, removedDuplicates: [], addedEnv: [], gitignoreUpdated: false, }, - } as any); + } as ProcessComparisonResult); await scanUsage({ ...baseOpts, isCiMode: false, json: false }); @@ -279,7 +284,7 @@ describe('scanUsage', () => { }); vi.mocked(processComparisonFile).mockReturnValue({ error: { message: 'soft error', shouldExit: false }, - } as any); + } as ProcessComparisonResult); vi.mocked(printComparisonError).mockReturnValue({ exit: false }); const result = await scanUsage(baseOpts); @@ -295,6 +300,10 @@ describe('scanUsage', () => { vi.mocked(processComparisonFile).mockReturnValue({ scanResult: { ...baseScanResult }, comparedAgainst: '.env', + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], fix: { fixApplied: false, removedDuplicates: [], @@ -304,7 +313,7 @@ describe('scanUsage', () => { uppercaseWarnings: [{ key: 'myKey', suggestion: 'MY_KEY' }], expireWarnings: [{ key: 'OLD_KEY', date: '2024-01-01', daysLeft: -10 }], inconsistentNamingWarnings: [{ key1: 'A', key2: 'B', suggestion: 'A_B' }], - } as any); + } as ProcessComparisonResult); await scanUsage(baseOpts); @@ -337,6 +346,10 @@ describe('scanUsage', () => { vi.mocked(processComparisonFile).mockReturnValue({ scanResult: { ...baseScanResult }, comparedAgainst: DEFAULT_EXAMPLE_FILE, + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], exampleFull: { SECRET: 'abc123' }, fix: { fixApplied: false, @@ -344,7 +357,7 @@ describe('scanUsage', () => { addedEnv: [], gitignoreUpdated: false, }, - } as any); + } as ProcessComparisonResult); await scanUsage(baseOpts); @@ -361,6 +374,10 @@ describe('scanUsage', () => { vi.mocked(processComparisonFile).mockReturnValue({ scanResult: { ...baseScanResult }, comparedAgainst: '.env', + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], exampleFull: { SECRET: 'abc123' }, fix: { fixApplied: false, @@ -368,7 +385,7 @@ describe('scanUsage', () => { addedEnv: [], gitignoreUpdated: false, }, - } as any); + } as ProcessComparisonResult); await scanUsage(baseOpts); @@ -525,4 +542,92 @@ describe('scanUsage', () => { expect(result.exitWithError).toBe(true); } }); + + it('filters out usages on HTML comment closing lines (-->)', async () => { + vi.mocked(scanCodebase).mockResolvedValue({ + ...baseScanResult, + used: [ + { + variable: 'A', + file: 'f.ts', + line: 1, + column: 0, + pattern: 'process.env', + context: '--> process.env.A', + }, + ], + }); + vi.mocked(determineComparisonFile).mockResolvedValue({ type: 'none' }); + + await scanUsage({ ...baseOpts, isCiMode: true }); + + expect(printScanResult).toHaveBeenCalledWith( + expect.objectContaining({ used: [] }), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + + it('filters out the dotenv-diff-ignore-end line itself', async () => { + vi.mocked(scanCodebase).mockResolvedValue({ + ...baseScanResult, + used: [ + { + variable: 'END_LINE', + file: 'f.ts', + line: 1, + column: 0, + pattern: 'process.env', + context: '', + }, + ], + }); + vi.mocked(determineComparisonFile).mockResolvedValue({ type: 'none' }); + + await scanUsage({ ...baseOpts, isCiMode: true }); + + expect(printScanResult).toHaveBeenCalledWith( + expect.objectContaining({ used: [] }), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + + it('filters out the dotenv-diff-ignore-start line itself but not subsequent usages', async () => { + vi.mocked(scanCodebase).mockResolvedValue({ + ...baseScanResult, + used: [ + { + variable: 'START_LINE', + file: 'f.ts', + line: 1, + column: 0, + pattern: 'process.env', + context: '', + }, + { + variable: 'KEPT', + file: 'f.ts', + line: 2, + column: 0, + pattern: 'process.env', + context: 'process.env.KEPT', + }, + ], + }); + vi.mocked(determineComparisonFile).mockResolvedValue({ type: 'none' }); + + await scanUsage({ ...baseOpts, isCiMode: true }); + + expect(printScanResult).toHaveBeenCalledWith( + expect.objectContaining({ + used: [expect.objectContaining({ variable: 'KEPT' })], + }), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); }); diff --git a/packages/cli/test/unit/core/security/secretDetectors.test.ts b/packages/cli/test/unit/core/security/secretDetectors.test.ts index 411ffb2..58f3b30 100644 --- a/packages/cli/test/unit/core/security/secretDetectors.test.ts +++ b/packages/cli/test/unit/core/security/secretDetectors.test.ts @@ -367,6 +367,28 @@ const token = "AKIAIOSFODNN7EXAMPLE"; expect(findings).toHaveLength(0); }); + it('upgrades severity from medium to high when same line matches both suspicious key and provider pattern', () => { + // "secret" triggers SUSPICIOUS_KEYS (medium), "ghp_..." triggers PROVIDER_PATTERNS (high) + // Both are kind: 'pattern' on same line → dedup should keep the high severity one + const source = + 'const secret = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";'; + const findings = detectSecretsInSource('test.ts', source); + + expect(findings).toHaveLength(1); + expect(findings[0].severity).toBe('high'); + expect(findings[0].message).toContain('known provider key pattern'); + }); + + it('does not downgrade severity when a lower severity finding appears after a higher one', () => { + // If somehow high came first and medium second, high should be kept + // provider pattern (high) + suspicious key (medium) on same line → stays high + const source = 'const apikey = "AKIAIOSFODNN7EXAMPLE";'; + const findings = detectSecretsInSource('test.ts', source); + + expect(findings).toHaveLength(1); + expect(findings[0].severity).toBe('high'); + }); + it('should use higher threshold for test files', () => { const source = 'const key = "aB3dE5fG7hI9jK0lM2nO4pQ6rS8tU1vW3xY5zA7bC9dE1fG3hI5jK7lM9nO0pQ2";'; @@ -476,13 +498,15 @@ const email = "user@example.com"; }); it('still flags hardcoded secret in JSX prop string literal', () => { - const source = ''; + const source = + ''; const findings = detectSecretsInSource('Component.tsx', source); expect(findings.length).toBeGreaterThan(0); }); it('still flags hardcoded token in JSX prop expression string', () => { - const source = ''; + const source = + ''; const findings = detectSecretsInSource('Component.tsx', source); expect(findings.length).toBeGreaterThan(0); }); diff --git a/packages/cli/test/unit/services/detectEnvExpirations.test.ts b/packages/cli/test/unit/services/detectEnvExpirations.test.ts index 06b7d90..c220e5b 100644 --- a/packages/cli/test/unit/services/detectEnvExpirations.test.ts +++ b/packages/cli/test/unit/services/detectEnvExpirations.test.ts @@ -147,4 +147,32 @@ describe('detectEnvExpirations', () => { }, ]); }); + + it('skips env key when expire date has invalid month or day (e.g. 00)', () => { + fs.writeFileSync( + envPath, + ` + # @expire 2024-00-01 + API_KEY=123 + `, + ); + + const result = detectEnvExpirations(envPath); + + expect(result).toEqual([]); + }); + + it('skips env key when expire date has zero day', () => { + fs.writeFileSync( + envPath, + ` + # @expire 2024-12-00 + API_KEY=123 + `, + ); + + const result = detectEnvExpirations(envPath); + + expect(result).toEqual([]); + }); });