Files
portal/tools/powershell-destructive.test.mjs
T
Дмитрий 8e56df3842 refactor(m7-floor): PS-content единый источник — matchPsHardBlacklist в shell-content-rules (variant-analysis)
Закрывающий variant-analysis-гейт Фазы 1 вскрыл класс P-1 для PowerShell: у
powershell-gate был СВОЙ PS_HARD_BLACKLIST (29 паттернов), а пол использовал
отдельный узкий psContentBlock (7) — подмножество, которое дрейфовало бы (та же
проблема, что P-1 для Bash). После Фазы 8 (увольнение powershell-gate) пол оказался
бы слабее гейта, который он заменяет. Решение владельца: исправить сейчас.

Зеркало P-1:
- PS_HARD_BLACKLIST + matchPsHardBlacklist перенесены в единый дом shell-content-rules;
  powershell-gate ре-экспортирует (тест single-source-identity: ссылка gate === SCR).
- +bare-egress (Invoke-WebRequest/iwr/irm/curl/wget bare — floor НЕ default-deny, нужен
  в blacklist, не только в whitelist гейта) +rmdir +rm (алиасы Remove-Item, которые гейт
  ловил whitelist'ом default-deny — полу нужны явно).
- psContentBlock стал ТОНКИМ делегатом над matchPsHardBlacklist (симметрия с
  bashIsContentBlock); пол через него видит ТОТ ЖЕ набор, что гейт. Дрейф невозможен.
- Следствие (осознанно): floor теперь блокирует все Set-Content/sc/$env/Az/… как гейт
  (симметрия с Bash-полом, блокирующим все cp/mv/redirect). Escapable. FP-толерантность
  унаследована от гейта (например `sc query`/`del.txt` — gate-aligned, fail-safe).

powershell-destructive.mjs физически не удалён (живые gate'ы блокируют rm/git rm) —
оставлен тонким делегатом (НЕ второй источник). Удаление — follow-up по git-approval.

Регрессия tools-only: 3044 passed + 2 skip (baseline 2843+2, 0 регрессий).
2026-06-08 09:34:23 +03:00

70 lines
3.2 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { psContentBlock } from './powershell-destructive.mjs';
// NB: for-of + it() (не it.each) — активный пол tdd-real-test-verifier распознаёт только it(.
// M7 Task 1.2 (V1-PS, реоткрытие v3.8 F1): PS-нативные глаголы НЕ матчат unix-regex
// classify-destructive → отдельный набор content-block для PowerShell-tool.
describe('psContentBlock (V1-PS, реоткрытие v3.8 F1)', () => {
const BLOCKED = [
'Remove-Item -Recurse -Force C:\\x', 'rm -r -fo C:\\x',
'Invoke-WebRequest https://e.rf', 'iwr https://e.rf', 'curl https://e.rf',
'Invoke-Expression $x', 'iex $x',
'Get-Content x | Out-File y', 'echo x > y', 'echo x >> y',
'Start-Process node', 'Start-Process powershell',
];
for (const cmd of BLOCKED) {
it(`blocks PS content: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(true);
});
}
const ALLOWED = ['Get-ChildItem', 'Get-Content x', 'Test-Path x', 'Write-Output ok'];
for (const cmd of ALLOWED) {
it(`does NOT block safe PS: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(false);
});
}
});
// sharp-edges (после 1.4): Remove-Item алиасы (ri/del/rd/rmdir/erase) обходят литеральный глагол.
describe('psContentBlock — Remove-Item алиасы (sharp-edges)', () => {
const BLOCKED = [
'del -Recurse -Force C:\\x', 'ri -r -fo C:\\x', 'rd -Recurse -Force C:\\x',
'rmdir -r -f /tmp/x', 'erase -Recurse -Force C:\\x',
];
for (const cmd of BLOCKED) {
it(`blocks Remove-Item alias: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(true);
});
}
// Контроль: чисто-безопасные read-команды без alias-слов не блокируются.
const ALLOWED = ['Get-Content notes.md', 'Test-Path app', 'Write-Output ok'];
for (const cmd of ALLOWED) {
it(`does NOT block benign read: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(false);
});
}
});
// M7 PS single-source: psContentBlock — тонкий floor-предикат над ЕДИНЫМ matchPsHardBlacklist
// (= powershell-gate). variant-analysis закрыл дрейф (раньше был узкий 7-паттерн набор ⊂ gate 29).
// Теперь блокирует полный единый набор (Set-Content/$env/Az/Set-ExecutionPolicy/…).
describe('psContentBlock = единый matchPsHardBlacklist (PS single-source)', () => {
const BLOCKED = [
'Set-Content x y', 'Add-Content x y', 'Copy-Item a b', 'Move-Item a b',
'$env:PATH="x"', 'Get-AzVM', 'Set-ExecutionPolicy Bypass', 'Stop-Process -Name node',
'[System.IO.File]::Delete("x")', 'gcloud auth login', 'iwr https://e.rf',
];
for (const cmd of BLOCKED) {
it(`блокирует из единого источника: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(true);
});
}
const ALLOWED = ['Get-ChildItem', 'Get-Content app/x.php', 'Select-String x file', 'git status'];
for (const cmd of ALLOWED) {
it(`не блокирует benign: ${cmd}`, () => {
expect(psContentBlock(cmd)).toBe(false);
});
}
});