import { describe, it, expect } from 'vitest'; import { tokenizePowerShell, matchPsHardBlacklist } from './enforce-powershell-gate.mjs'; describe('tokenizePowerShell', () => { it('splits on ; and | into segments', () => { const segs = tokenizePowerShell('Get-Content a | Select-String x ; Get-Item b'); expect(segs.map((s) => s.cmd)).toEqual(['get-content', 'select-string', 'get-item']); }); }); describe('matchPsHardBlacklist — keep', () => { it.each([ 'Remove-Item x', 'ri x', 'del x', 'Move-Item a b', 'Copy-Item a b', 'Set-Content x "y"', 'Add-Content x "y"', 'Out-File -FilePath x', 'cmd > out.txt', 'Invoke-Expression $x', 'iex $x', 'Start-Process notepad', '[System.IO.File]::Delete("x")', 'Stop-Process -Name node', 'Set-ExecutionPolicy Bypass', 'icacls x /grant y', ])('blocks %s', (cmd) => { expect(matchPsHardBlacklist(cmd)).toBeTruthy(); }); }); describe('matchPsHardBlacklist — v4.1 G10', () => { it.each([ '$env:PATH = "x"', '$env:ROUTER_LLM_KEY="leak"', '[System.Environment]::SetEnvironmentVariable("X","Y")', 'Set-Item -Path Env:FOO -Value bar', 'New-PSDrive -Name X -PSProvider FileSystem -Root C:\\', 'Get-AzVM', 'New-AzResourceGroup x', 'Get-AWSCredential', 'gcloud auth login', ])('blocks %s', (cmd) => { expect(matchPsHardBlacklist(cmd)).toBeTruthy(); }); }); describe('matchPsHardBlacklist — allows benign', () => { it.each(['Get-ChildItem', 'Get-Content app/x.php', 'Select-String x file', 'git status'])('allows %s', (cmd) => { expect(matchPsHardBlacklist(cmd)).toBe(null); }); }); import { classifyPowerShellCommand } from './enforce-powershell-gate.mjs'; describe('classifyPowerShellCommand', () => { const now = 4_000_000; it('allows whitelisted reading cmdlet', () => { expect(classifyPowerShellCommand('Get-ChildItem -Path app', {}).result).toBe('allow'); }); it('allows alias gci', () => { expect(classifyPowerShellCommand('gci', {}).result).toBe('allow'); }); it('blocks hard-blacklisted Remove-Item', () => { expect(classifyPowerShellCommand('Remove-Item x', {}).result).toBe('block'); }); it('blocks G10 $env set', () => { expect(classifyPowerShellCommand('$env:PATH="x"', {}).result).toBe('block'); }); it('blocks reading a protected path', () => { expect(classifyPowerShellCommand('Get-Content ~/.claude/settings.json', {}).result).toBe('block'); }); it('routes git through shared classifier (block unapproved commit)', () => { expect(classifyPowerShellCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block'); }); it('allows readonly git through PowerShell', () => { expect(classifyPowerShellCommand('git status', {}).result).toBe('allow'); }); it('default-denies unknown cmdlet', () => { expect(classifyPowerShellCommand('Frobnicate-Thing', {}).result).toBe('block'); }); }); // M7 PS single-source: powershell-gate ре-экспортирует матчер из единого дома shell-content-rules. // Идентичность ссылки доказывает «единый источник, не копия» (зеркало Bash P-1). content-floor (М5) // импортирует ТОТ ЖЕ матчер → дрейф подмножествами невозможен по конструкции. import { matchPsHardBlacklist as PG_MATCH, PS_HARD_BLACKLIST as PG_LIST } from './enforce-powershell-gate.mjs'; import { matchPsHardBlacklist as SCR_PS_MATCH, PS_HARD_BLACKLIST as SCR_PS_LIST } from './shell-content-rules.mjs'; describe('powershell-gate re-exports single-source PS matcher (M7)', () => { it('matchPsHardBlacklist is the SAME reference as shell-content-rules', () => { expect(PG_MATCH).toBe(SCR_PS_MATCH); }); it('PS_HARD_BLACKLIST is the SAME reference as shell-content-rules', () => { expect(PG_LIST).toBe(SCR_PS_LIST); }); });