( filePath: string, sensitiveValues: Map<string, string>, )
| 253 | * sensitiveValues is a map from env key name to its resolved string value. |
| 254 | */ |
| 255 | export async function scanFileForValues( |
| 256 | filePath: string, |
| 257 | sensitiveValues: Map<string, string>, |
| 258 | ): Promise<Array<ScanFinding>> { |
| 259 | const findings: Array<ScanFinding> = []; |
| 260 | let content: string; |
| 261 | try { |
| 262 | content = await fs.readFile(filePath, 'utf-8'); |
| 263 | } catch { |
| 264 | return findings; |
| 265 | } |
| 266 | |
| 267 | // Skip binary files (contain null bytes) |
| 268 | if (content.includes('\0')) return findings; |
| 269 | |
| 270 | // Quick pre-check: skip the file entirely if none of the values appear |
| 271 | let anyMatch = false; |
| 272 | for (const val of sensitiveValues.values()) { |
| 273 | if (content.includes(val)) { |
| 274 | anyMatch = true; |
| 275 | break; |
| 276 | } |
| 277 | } |
| 278 | if (!anyMatch) return findings; |
| 279 | |
| 280 | const lines = content.split('\n'); |
| 281 | for (let i = 0; i < lines.length; i++) { |
| 282 | const line = lines[i]; |
| 283 | for (const [keyName, val] of sensitiveValues) { |
| 284 | const colIdx = line.indexOf(val); |
| 285 | if (colIdx !== -1) { |
| 286 | findings.push({ |
| 287 | filePath, |
| 288 | lineNumber: i + 1, |
| 289 | columnNumber: colIdx + 1, |
| 290 | line: line.trim(), |
| 291 | sensitiveKeyName: keyName, |
| 292 | sensitiveValue: val, |
| 293 | }); |
| 294 | break; // one finding per line is enough |
| 295 | } |
| 296 | } |
| 297 | } |
| 298 | return findings; |
| 299 | } |
| 300 | |
| 301 | const SCAN_COMMAND = 'varlock scan'; |
| 302 |
no test coverage detected