-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathscanCodebase.ts
More file actions
118 lines (104 loc) · 3.6 KB
/
scanCodebase.ts
File metadata and controls
118 lines (104 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import fs from 'fs/promises';
import path from 'path';
import type { EnvUsage, ScanOptions, ScanResult } from '../config/types.js';
import {
detectSecretsInSource,
type SecretFinding,
} from '../core/security/secretDetectors.js';
import { DEFAULT_EXCLUDE_PATTERNS } from '../core/scan/patterns.js';
import { scanFile } from '../core/scan/scanFile.js';
import { findFiles } from './fileWalker.js';
import { normalizePath } from '../core/helpers/normalizePath.js';
import { isLikelyMinified } from '../core/helpers/isLikelyMinified.js';
/**
* Scans the codebase for environment variable usage based on the provided options.
* @param opts - Options for scanning the codebase.
* @returns A promise that resolves to the scan result containing used, missing, and unused variables.
*/
export async function scanCodebase(opts: ScanOptions): Promise<ScanResult> {
const files = await findFiles(opts.cwd, {
include: opts.include,
exclude: [...DEFAULT_EXCLUDE_PATTERNS, ...opts.exclude],
...(opts.files && opts.files.length > 0 && { files: opts.files }),
});
const allUsages: EnvUsage[] = [];
let filesScanned = 0;
const allSecrets: SecretFinding[] = [];
const fileContentMap = new Map<string, string>();
for (const filePath of files) {
const content = await safeReadFile(filePath);
if (!content) continue;
if (isLikelyMinified(content)) continue; // Skip likely minified files
// Scan the file for environment variable usages
const fileUsages = scanFile(filePath, content, opts);
allUsages.push(...fileUsages);
// Store file content for later use (e.g., framework validation 'use client')
const relativePath = normalizePath(path.relative(opts.cwd, filePath));
fileContentMap.set(relativePath, content);
// Detect secrets in the file content
const secrets = safeDetectSecrets(relativePath, content, opts);
if (secrets.length) allSecrets.push(...secrets);
// Count successfully scanned files
filesScanned++;
}
// Filter out ignored variables
const filteredUsages = allUsages.filter(
(usage) =>
!opts.ignore.includes(usage.variable) &&
!opts.ignoreRegex.some((regex) => regex.test(usage.variable)),
);
const uniqueVariables = [...new Set(filteredUsages.map((u) => u.variable))];
const loggedVariables = filteredUsages.filter((u) => u.isLogged);
return {
used: filteredUsages,
missing: [],
unused: [],
secrets: allSecrets,
stats: {
filesScanned,
totalUsages: filteredUsages.length,
uniqueVariables: uniqueVariables.length,
warningsCount: 0,
duration: 0,
},
duplicates: {
env: [],
example: [],
},
logged: loggedVariables,
fileContentMap,
};
}
/**
* Detects secrets in the given file content if secret detection is enabled.
* @param relativePath - The relative path of the file being scanned.
* @param content - The content of the file.
* @param opts - The scan options.
* @returns An array of secret findings.
*/
function safeDetectSecrets(
relativePath: string,
content: string,
opts: ScanOptions,
): SecretFinding[] {
if (!opts.secrets) return [];
try {
return detectSecretsInSource(relativePath, content, opts).filter(
(s) => s.severity !== 'low',
);
} catch {
return [];
}
}
/**
* Safely reads a file and returns its content or null if reading fails.
* @param filePath - The path to the file to read.
* @returns The file content as a string, or null if an error occurs.
*/
async function safeReadFile(filePath: string): Promise<string | null> {
try {
return await fs.readFile(filePath, 'utf-8');
} catch {
return null;
}
}