# sample_secret.mjs

## Огляд

Модуль `sample_secret.mjs` — це FS-частина правила `security`, концерн `sample_secret`. Він реалізує перевірку, що фейкові credential-значення у **прикладних** файлах репозиторію записані як канонічний placeholder `sample-secret`, а не як bare `secret`.

Мотивація: рядок `sample-secret` містить підрядок `sample`, який є у вшитому списку `DefaultFalsePositives` сканера TruffleHog. Завдяки цьому таке значення гарантовано відсіюється сканером незалежно від його версії. Bare `secret` наразі не фіксується TruffleHog лише через випадкову присутність у словнику `fp_words.txt`, що є крихкою поведінкою, залежною від версії інструмента — покладатися на неї не можна.

«Прикладними» вважаються файли, що відповідають хоча б одному з критеріїв:

- basename має суфікс `.example`, `.sample`, `.template` або `.dist` (наприклад, `.env.dist`, `config.example`);
- basename має infix `.example.`, `.sample.` або `.template.` (наприклад, `docker-compose.example.yml`, `app.config.sample.json`);
- файл лежить усередині каталогу `fixtures`, `fixture` або `__fixtures__` (на будь-якому рівні дерева).

Решта файлів не сканується — там слово `secret` майже завжди є частиною реального коду, а не placeholder.

Порушенням є лише `secret` у **позиції значення** — одразу після `=`, `:` чи `=>` (з опційними одинарними або подвійними лапками). Імена ключів (`client_secret`, `JWT_SECRET`) свідомо не чіпаються: матч прив'язаний до значення, а не до ключа.

Архітектурний вибір (regex замість AST) пояснюється тим, що прикладні файли — це різнорідні конфіги (`.env`, YAML, JSON, TOML, plain `.dist`), єдиного AST для них немає, тож скан виконується порядково. Вибір JavaScript замість Rego зумовлений необхідністю обходу дерева (`readdir`) для пошуку прикладних файлів, тоді як conftest/Rego парсить лише структуровані документи.

## Експорти / API

Файл є ES-модулем (`.mjs`) і експортує одну публічну функцію:

| Експорт | Тип                                       | Призначення                                                       |
| ------- | ----------------------------------------- | ----------------------------------------------------------------- |
| `check` | `async (cwd?: string) => Promise<number>` | Запускає перевірку та повертає exit-код (0 — OK, 1 — є порушення) |

Внутрішні артефакти модуля (не експортуються):

- `EXAMPLE_SUFFIX_RE` — RegExp для перевірки суфікса basename'а прикладного файлу.
- `EXAMPLE_INFIX_RE` — RegExp для перевірки infix'а у basename'і.
- `FIXTURE_DIR_RE` — RegExp для виявлення сегмента шляху з фікстурами.
- `VALUE_SECRET_RE` — RegExp для виявлення bare-`secret` у позиції значення.
- `isExampleFile(relPosix)` — допоміжна функція класифікації файлу.

## Функції

### `isExampleFile(relPosix)`

**Сигнатура:** `function isExampleFile(relPosix: string): boolean`

**Параметри:**

- `relPosix` (`string`) — відносний шлях файлу від кореня репозиторію у POSIX-форматі (роздільник `/`, не `\`).

**Повертає:** `boolean` — `true`, якщо файл слід сканувати на наявність bare `secret`.

**Логіка:**

1. Виокремлює basename з `relPosix` шляхом обрізання за останнім `/`.
2. Перевіряє три умови (логічне АБО):
   - `EXAMPLE_SUFFIX_RE.test(base)` — basename закінчується на `.example`, `.sample`, `.template` або `.dist`.
   - `EXAMPLE_INFIX_RE.test(base)` — basename містить `.example.`, `.sample.` або `.template.`.
   - `FIXTURE_DIR_RE.test(relPosix)` — повний відносний шлях містить сегмент `fixtures`, `fixture` або `__fixtures__`.
3. Повертає результат диз'юнкції.

**Side effects:** немає (чиста функція, працює лише з аргументом).

### `check(cwd?)`

**Сигнатура:** `async function check(cwd?: string): Promise<number>`

**Параметри:**

- `cwd` (`string`, опційний) — абсолютний шлях до кореня репозиторію. За замовчуванням — `process.cwd()`.

**Повертає:** `Promise<number>` — exit-код перевірки:

- `0` — порушень не знайдено (або прикладних файлів не знайдено взагалі);
- `1` — знайдено хоча б один bare `secret` у позиції значення.

**Алгоритм:**

1. Створює репортер через `createCheckReporter()` і деструктурує з нього методи `pass` і `fail`.
2. Ініціалізує масив `examples` для зберігання пар `{ abs, rel }` прикладних файлів.
3. Викликає `walkDir(cwd, callback)` — рекурсивний обхід дерева від `cwd`. У callback'у:
   - обчислює відносний шлях `rel` через `relative(cwd, abs)`;
   - нормалізує роздільники до `/` (заміна `sep` на `/`) — це важливо на Windows;
   - якщо `isExampleFile(rel)` повертає `true`, додає об'єкт у `examples`.
4. Сортує `examples` за `rel` у лексикографічному порядку (`localeCompare`) — детермінує вихід.
5. Якщо `examples.length === 0`, репортує `pass('прикладних файлів не знайдено — placeholder перевіряти нема де')` і повертає exit-код репортера.
6. Інакше для кожного файлу:
   - читає вміст через `readFile(abs, 'utf8')`;
   - при будь-якій помилці читання (`try/catch` без re-throw) файл мовчки пропускається — `continue`;
   - розбиває вміст на рядки за `\n`;
   - для кожного рядка обрізає завершальний `\r` (CRLF-нормалізація);
   - тестує рядок проти `VALUE_SECRET_RE`; якщо матч є:
     - інкрементує лічильник `violations`;
     - викликає `fail` з повідомленням формату `${rel}:${i + 1}: \`${line.trim()}\` — заміни placeholder \`secret\` на \`sample-secret\` (security.mdc)` (нумерація рядків — 1-based).
7. Якщо після обходу `violations === 0`, репортує `pass('прикладні файли (${examples.length}) не містять bare \`secret\`')`.
8. Повертає `reporter.getExitCode()`.

**Side effects:**

- Читання файлової системи: рекурсивний `walkDir` і `readFile` для кожного прикладного файлу.
- Виклики `pass`/`fail` репортера, який пише повідомлення у stdout/stderr (поведінка інкапсульована в `createCheckReporter`).

**Обробка помилок:** помилки `readFile` ловляться і ігноруються (continue). Це дозволяє пропускати файли без прав читання чи з тимчасовими IO-проблемами без падіння всієї перевірки.

## Залежності

### Стандартна бібліотека Node.js

| Імпорт     | Шлях               | Призначення                                                        |
| ---------- | ------------------ | ------------------------------------------------------------------ |
| `readFile` | `node:fs/promises` | Асинхронне читання файлу як UTF-8 рядка                            |
| `relative` | `node:path`        | Обчислення відносного шляху від `cwd` до абсолютного шляху         |
| `sep`      | `node:path`        | Платформозалежний роздільник шляхів (`/` на POSIX, `\` на Windows) |

### Внутрішні модулі проєкту

| Імпорт                | Шлях                                      | Призначення                                                                                             |
| --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `createCheckReporter` | `../../../scripts/lib/check-reporter.mjs` | Створює об'єкт-репортер з методами `pass`, `fail`, `getExitCode` для уніфікованого виводу check-функцій |
| `walkDir`             | `../../../scripts/utils/walkDir.mjs`      | Рекурсивний обхід директорії з застосуванням callback'а до кожного файлу                                |

### Регулярні вирази

Усі RegExp мають флаги `u` (Unicode) і, де потрібно, `i` (case-insensitive):

- `EXAMPLE_SUFFIX_RE = /\.(?:example|sample|template|dist)$/iu` — суфікс basename'а.
- `EXAMPLE_INFIX_RE = /\.(?:example|sample|template)\./iu` — infix у basename'і (без `dist`, бо `dist` доречно лише як кінцевий суфікс).
- `FIXTURE_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?)(?:\/|$)/u` — сегмент шляху з фікстурами (без флага `i`, бо назви каталогів зазвичай у нижньому регістрі).
- `VALUE_SECRET_RE = /[:=]>?\s*(['"]?)secret\1[\s,;}\])]*(?:(?:#|\/\/).*)?$/iu`:
  - `[:=]>?` — `=`, `:` або `=>`;
  - `\s*` — опційні пробіли;
  - `(['"]?)secret\1` — слово `secret` з опційними **парними** лапками (`'`, `"` або без);
  - `[\s,;}\])]*` — опційний хвіст (пробіли, кома, крапка з комою, закриваючі дужки);
  - `(?:(?:#|\/\/).*)?$` — опційний коментар (`#` для shell/YAML/TOML, `//` для JS/JSON5) до кінця рядка;
  - `$` гарантує, що `secret` — увесь токен значення (`secret-key`, `secretValue` не матчаться);
  - `[:=]` як префікс відсікає імена ключів (`client_secret`).

## Потік виконання / Використання

### Типовий сценарій запуску

Модуль є точкою FS-частини правила `security/sample_secret`. Викликається оркестратором правил (зазвичай через CLI або CI), який:

1. Імпортує функцію `check` з `npm/rules/security/js/sample_secret.mjs`.
2. Викликає `await check(repoRoot)`, де `repoRoot` — абсолютний шлях до кореня монорепо.
3. Передає отриманий exit-код у власний агрегатор результатів.

Приклад прямого виклику з CLI-обгортки:

```js
import { check } from './sample_secret.mjs'

const code = await check(process.cwd())
process.exit(code)
```

### Послідовність дій усередині `check`

```
walkDir(cwd) ──► фільтр isExampleFile ──► examples[] (відсортовано)
       │
       ▼
для кожного файлу:
   readFile ──► split('\n') ──► для кожного рядка:
      ├─ обрізати \r
      ├─ VALUE_SECRET_RE.test(line)?
      │    ├─ ні  → пропустити
      │    └─ так → fail(rel:line: ...); violations++
       ▼
violations === 0 ? pass(...) : (вже були fail)
       ▼
return reporter.getExitCode()
```

### Інтеграція з системою правил

- Файл лежить у `npm/rules/security/js/`, що є FS-частиною модульного правила `security` (концерн `sample_secret`).
- Поряд може існувати Rego/conftest-частина для структурованих перевірок; цей файл відповідає лише за неструктуроване сканування тексту.
- Репортер `createCheckReporter` повертає exit-код, який інтегрується з загальним пайплайном лінтерів/перевірок проєкту.

### Поведінкові гарантії

- **Детермінізм:** сортування `examples` за `rel.localeCompare` гарантує однаковий порядок повідомлень `fail` на різних запусках.
- **Кросплатформність:** нормалізація `sep → '/'` забезпечує єдиний формат шляхів у логах і регекспах незалежно від ОС.
- **Стійкість:** помилки `readFile` ловляться — недоступний файл не валить перевірку.
- **Точність:** прив'язки `[:=]` зліва і `$` справа у `VALUE_SECRET_RE` мінімізують false positives — імена ключів, складені значення (`secret-key`) і слово `secret` у середині рядка не матчаться.

### Що НЕ робить цей модуль

- Не сканує не-прикладні файли (звичайний код, документація поза `fixtures/`).
- Не перевіряє імена ключів (`client_secret`, `JWT_SECRET` дозволені).
- Не автоматично замінює `secret` на `sample-secret` — лише репортує; виправлення робить розробник вручну.
- Не парсить структуровані формати (YAML/JSON/TOML) — це робота Rego-частини правила, якщо вона існує.
- Не виконує реальну детекцію секретів — це робота TruffleHog; модуль лише гарантує сумісність placeholder'ів із вшитим whitelist'ом TruffleHog.
