|
|
|
|
@ -76,14 +76,38 @@ const BASH_BLACKLIST_RE = new RegExp(
@@ -76,14 +76,38 @@ const BASH_BLACKLIST_RE = new RegExp(
|
|
|
|
|
'i' |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Safe-list: только эти команды не требуют подтверждения. `type` убран — читает
|
|
|
|
|
// файлы в обход validatePath (включая .env). Любые policy-флаги отменяют allow.
|
|
|
|
|
const SAFE_PLAIN_HEADS = new Set(['pwd', 'dir', 'ls', 'tree', 'where', 'whoami']); |
|
|
|
|
const SAFE_GIT_READONLY = new Set(['status', 'diff', 'log', 'show', 'branch', 'rev-parse', 'ls-files', 'describe', 'remote']); |
|
|
|
|
// Safe-list: кроссплатформенный (Windows + Unix). Только команды, которые
|
|
|
|
|
// НЕ читают содержимое файлов (иначе обход validatePath и доступ к .env/ключам).
|
|
|
|
|
// cat, head, tail, type, more, less — сознательно НЕ включены: они читают файлы.
|
|
|
|
|
// Любые policy-флаги (shell, destructive, absolute-path, parent-path) отменяют allow.
|
|
|
|
|
const SAFE_PLAIN_HEADS = new Set([ |
|
|
|
|
'pwd', 'dir', 'ls', 'tree', 'where', 'which', 'whoami', 'hostname', |
|
|
|
|
'uname', 'ver', 'cd', 'echo', 'date', 'time' |
|
|
|
|
]); |
|
|
|
|
const SAFE_GIT_READONLY = new Set(['status', 'diff', 'log', 'show', 'branch', 'rev-parse', 'ls-files', 'describe', 'remote', 'config']); |
|
|
|
|
const SAFE_PKG_HEADS = new Set(['npm', 'pnpm', 'yarn', 'bun']); |
|
|
|
|
const SAFE_PKG_SECOND = new Set(['test', 'run', 'build', 'lint', 'typecheck', 'check', '--version', '-v', 'ls', 'list', 'why', 'outdated']); |
|
|
|
|
const SAFE_PKG_SECOND = new Set(['test', 'run', 'build', 'lint', 'typecheck', 'check', '--version', '-v', 'ls', 'list', 'why', 'outdated', 'info', 'view']); |
|
|
|
|
const SAFE_NPX_TOOLS = new Set(['tsc', 'eslint', 'prettier', 'vitest', 'jest', 'mocha']); |
|
|
|
|
|
|
|
|
|
// Глобальные флаги, которые стоят ПЕРЕД подкомандой и не меняют её смысла.
|
|
|
|
|
// git -C <path> status -> подкоманда status, safe.
|
|
|
|
|
// npm --prefix <path> test -> подкоманда test, safe.
|
|
|
|
|
const GIT_GLOBAL_WITH_ARG = new Set(['-C', '-c']); |
|
|
|
|
const GIT_GLOBAL_NO_ARG = new Set(['--no-pager', '--bare', '--literal-pathspecs', '--no-optional-locks']); |
|
|
|
|
const PKG_GLOBAL_WITH_ARG = new Set(['--prefix', '--workspace', '-w']); |
|
|
|
|
|
|
|
|
|
function skipGlobalFlags(tokens, start, withArg, noArg) { |
|
|
|
|
let i = start; |
|
|
|
|
while (i < tokens.length) { |
|
|
|
|
const t = tokens[i]; |
|
|
|
|
if (noArg.has(t)) { i++; continue; } |
|
|
|
|
if (withArg.has(t)) { i += 2; continue; } |
|
|
|
|
if (t.startsWith('--') && t.includes('=')) { i++; continue; } |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
return i; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function computePolicyFlags(raw) { |
|
|
|
|
const flags = []; |
|
|
|
|
if (BASH_MULTILINE.test(raw)) flags.push('multiline'); |
|
|
|
|
@ -95,14 +119,31 @@ function computePolicyFlags(raw) {
@@ -95,14 +119,31 @@ function computePolicyFlags(raw) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function isSafeListed(normalized) { |
|
|
|
|
const tokens = normalized.split(' '); |
|
|
|
|
const tokens = normalized.split(' ').filter(Boolean); |
|
|
|
|
if (tokens.length === 0) return false; |
|
|
|
|
const head = tokens[0]; |
|
|
|
|
const second = tokens[1] || ''; |
|
|
|
|
if (SAFE_PLAIN_HEADS.has(head)) return true; |
|
|
|
|
if (head === 'git' && SAFE_GIT_READONLY.has(second)) return true; |
|
|
|
|
if (SAFE_PKG_HEADS.has(head) && SAFE_PKG_SECOND.has(second)) return true; |
|
|
|
|
if ((head === 'node' || head === 'npm' || head === 'npx' || head === 'pnpm' || head === 'yarn' || head === 'bun') && (second === '--version' || second === '-v')) return true; |
|
|
|
|
if (head === 'npx' && SAFE_NPX_TOOLS.has(second)) return true; |
|
|
|
|
if (head === 'git') { |
|
|
|
|
const idx = skipGlobalFlags(tokens, 1, GIT_GLOBAL_WITH_ARG, GIT_GLOBAL_NO_ARG); |
|
|
|
|
const sub = tokens[idx] || ''; |
|
|
|
|
return SAFE_GIT_READONLY.has(sub); |
|
|
|
|
} |
|
|
|
|
if (SAFE_PKG_HEADS.has(head)) { |
|
|
|
|
const idx = skipGlobalFlags(tokens, 1, PKG_GLOBAL_WITH_ARG, GIT_GLOBAL_NO_ARG); |
|
|
|
|
const sub = tokens[idx] || ''; |
|
|
|
|
if (SAFE_PKG_SECOND.has(sub)) return true; |
|
|
|
|
if (sub === '--version' || sub === '-v') return true; |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
if (head === 'node' || head === 'npx') { |
|
|
|
|
const second = tokens[1] || ''; |
|
|
|
|
if (second === '--version' || second === '-v') return true; |
|
|
|
|
if (head === 'npx') { |
|
|
|
|
const idx = skipGlobalFlags(tokens, 1, new Set(['-p', '--package']), new Set(['--yes', '-y', '--no-install'])); |
|
|
|
|
return SAFE_NPX_TOOLS.has(tokens[idx] || ''); |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
if (head === 'tsc' || head === 'eslint') return true; |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|