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