* Read all team memory files from the local directory into a flat key-value map. * Keys are relative paths from the team memory directory. * Empty files are included (content will be empty string). * * PSR M22174: Each file is scanned for credentials before inclusion * using patterns from gitle
(maxEntries: number | null)
| 565 | * warn the user. |
| 566 | */ |
| 567 | async function readLocalTeamMemory(maxEntries: number | null): Promise<{ |
| 568 | entries: Record<string, string> |
| 569 | skippedSecrets: SkippedSecretFile[] |
| 570 | }> { |
| 571 | const teamDir = getTeamMemPath() |
| 572 | const entries: Record<string, string> = {} |
| 573 | const skippedSecrets: SkippedSecretFile[] = [] |
| 574 | |
| 575 | async function walkDir(dir: string): Promise<void> { |
| 576 | try { |
| 577 | const dirEntries = await readdir(dir, { withFileTypes: true }) |
| 578 | await Promise.all( |
| 579 | dirEntries.map(async entry => { |
| 580 | const fullPath = join(dir, entry.name) |
| 581 | if (entry.isDirectory()) { |
| 582 | await walkDir(fullPath) |
| 583 | } else if (entry.isFile()) { |
| 584 | try { |
| 585 | const stats = await stat(fullPath) |
| 586 | if (stats.size > MAX_FILE_SIZE_BYTES) { |
| 587 | logForDebugging( |
| 588 | `team-memory-sync: skipping oversized file ${entry.name} (${stats.size} > ${MAX_FILE_SIZE_BYTES} bytes)`, |
| 589 | { level: 'info' }, |
| 590 | ) |
| 591 | return |
| 592 | } |
| 593 | const content = await readFile(fullPath, 'utf8') |
| 594 | const relPath = relative(teamDir, fullPath).replaceAll('\\', '/') |
| 595 | |
| 596 | // PSR M22174: scan for secrets BEFORE adding to the upload |
| 597 | // payload. If a secret is detected, skip this file entirely |
| 598 | // so it never leaves the machine. |
| 599 | const secretMatches = scanForSecrets(content) |
| 600 | if (secretMatches.length > 0) { |
| 601 | // Report only the first match per file — one secret is |
| 602 | // enough to skip the file and we don't want to log more |
| 603 | // than necessary about credential locations. |
| 604 | const firstMatch = secretMatches[0]! |
| 605 | skippedSecrets.push({ |
| 606 | path: relPath, |
| 607 | ruleId: firstMatch.ruleId, |
| 608 | label: firstMatch.label, |
| 609 | }) |
| 610 | logForDebugging( |
| 611 | `team-memory-sync: skipping "${relPath}" — detected ${firstMatch.label}`, |
| 612 | { level: 'warn' }, |
| 613 | ) |
| 614 | return |
| 615 | } |
| 616 | |
| 617 | entries[relPath] = content |
| 618 | } catch { |
| 619 | // Skip unreadable files |
| 620 | } |
| 621 | } |
| 622 | }), |
| 623 | ) |
| 624 | } catch (e) { |
no test coverage detected