111 lines
4.1 KiB
JavaScript
111 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Shared shell content rules для router-gate v4 (§5.1 + §5.1.2).
|
|
* Используется Bash-гейтом (enforce-router-gate.mjs) и PowerShell-гейтом
|
|
* (enforce-powershell-gate.mjs). Без хук-I/O — чистые функции + чтение
|
|
* approve-решений из ~/.claude/runtime.
|
|
*/
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { homedir } from 'os';
|
|
|
|
// ── Path normalization (Stream A заглушка; реальная — path-normalization.mjs) ──
|
|
export function defaultPathNormalize(target) {
|
|
if (typeof target !== 'string') return '';
|
|
let t = target.trim().replace(/^['"]|['"]$/g, '');
|
|
t = t.replace(/\\/g, '/');
|
|
const home = homedir().replace(/\\/g, '/');
|
|
t = t.replace(/^~(?=\/|$)/, home);
|
|
return t;
|
|
}
|
|
|
|
// Минимальный protected-list (полный — gate-config.json, Stream C/G).
|
|
export const DEFAULT_PROTECTED_PATTERNS = [
|
|
/(^|\/)\.claude\/runtime\//i,
|
|
/(^|\/)\.claude\/settings(\.local)?\.json$/i,
|
|
/(^|\/)\.env(\.|$)/i,
|
|
/(^|\/)node_modules\//i,
|
|
/(^|\/)CLAUDE\.md$/i,
|
|
/Pravila_raboty_Claude/i,
|
|
/Plugin_stack_rules/i,
|
|
/Tooling_v8_3/i,
|
|
/(^|\/)memory\//i,
|
|
/(^|\/)tools\/dep-checksums\.json$/i,
|
|
/(^|\/)\.git\/hooks\//i,
|
|
/(^|\/)lefthook\.ya?ml$/i,
|
|
/(^|\/)\.gitleaks/i,
|
|
/(^|\/)\.npmrc$/i,
|
|
];
|
|
|
|
export function isProtectedPath(p, pathNormalize = defaultPathNormalize, patterns = DEFAULT_PROTECTED_PATTERNS) {
|
|
const n = pathNormalize(p);
|
|
if (!n) return false;
|
|
return patterns.some((re) => re.test(n));
|
|
}
|
|
// ── generic helpers ──
|
|
export function normalizeCommand(cmd) {
|
|
return String(cmd || '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
export function matchAny(patterns, str) {
|
|
for (const { re, reason } of patterns) {
|
|
if (re.test(str)) return reason;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function extractPathArgs(tokens) {
|
|
if (!Array.isArray(tokens)) return [];
|
|
return tokens.slice(1).filter((t) => typeof t === 'string' && !t.startsWith('-') && t !== '>' && t !== '>>');
|
|
}
|
|
|
|
export function pathDenyOverlay({
|
|
candidatePaths = [],
|
|
pathNormalize = defaultPathNormalize,
|
|
protectedPaths = DEFAULT_PROTECTED_PATTERNS,
|
|
} = {}) {
|
|
for (const p of candidatePaths) {
|
|
if (isProtectedPath(p, pathNormalize, protectedPaths)) {
|
|
return { block: true, reason: `path-deny: доступ к защищённому пути «${pathNormalize(p)}» запрещён (§3.1)`, path: pathNormalize(p) };
|
|
}
|
|
}
|
|
return { block: false };
|
|
}
|
|
// ── #34 prompt-injection через echo/printf/Write-Output ──
|
|
export const INJECTION_PATTERNS = [
|
|
/\b(?:echo|printf|Write-Output|Write-Host)\s+["'][^"']*(?:делай|вызови|напиши Claude|скажи Claude|в следующем сообщении|следующий prompt|next prompt|ignore previous|игнорируй)/iu,
|
|
];
|
|
|
|
export function hasInjection(cmd) {
|
|
const s = String(cmd || '');
|
|
return INJECTION_PATTERNS.some((re) => re.test(s));
|
|
}
|
|
|
|
// ── approve_git_operation (Stream E пишет; мы читаем) ──
|
|
const APPROVE_WINDOW_MS = 5 * 60 * 1000;
|
|
|
|
export function isApproved(command, approvedGitOps, now = Date.now()) {
|
|
if (!Array.isArray(approvedGitOps) || approvedGitOps.length === 0) return false;
|
|
const target = normalizeCommand(command);
|
|
return approvedGitOps.some(
|
|
(op) => normalizeCommand(op.command) === target && typeof op.ts === 'number' && now - op.ts <= APPROVE_WINDOW_MS,
|
|
);
|
|
}
|
|
|
|
export function loadApprovedGitOps(sessionId, now = Date.now()) {
|
|
const path = join(homedir(), '.claude', 'runtime', `askuser-decisions-${sessionId || 'unknown'}.jsonl`);
|
|
if (!existsSync(path)) return [];
|
|
const out = [];
|
|
try {
|
|
const lines = readFileSync(path, 'utf-8').split(/\r?\n/);
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
let rec;
|
|
try { rec = JSON.parse(line); } catch { continue; }
|
|
if (rec && rec.type === 'approve_git_operation' && typeof rec.command === 'string') {
|
|
out.push({ command: rec.command, ts: typeof rec.ts === 'number' ? rec.ts : 0 });
|
|
}
|
|
}
|
|
} catch { return []; }
|
|
return out.filter((op) => now - op.ts <= APPROVE_WINDOW_MS);
|
|
} |