Skip to content

Commit 254c918

Browse files
committed
chore(gatekeeper): rebuild dist bundle for v2
1 parent de981f4 commit 254c918

1 file changed

Lines changed: 146 additions & 15 deletions

File tree

plugins/gatekeeper/dist/pre-tool-use.js

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function parseChainedCommand(cmd) {
125125
}
126126

127127
// src/pre-tool-use.ts
128-
var DENY_RULES = [
128+
var HARD_DENY_RULES = [
129129
{ pattern: /^rm\s+-rf\s+\/(?:\s|$)/i, reason: "Filesystem root deletion blocked" },
130130
{ pattern: /^rm\s+-rf\s+\/\*(?:\s|$)/i, reason: "Destructive wildcard deletion from root blocked" },
131131
{ pattern: /^rm\s+-rf\s+~(?:\/|$)/i, reason: "Home directory deletion blocked" },
@@ -151,6 +151,26 @@ var DENY_RULES = [
151151
reason: "find -exec/-execdir/-delete blocked: potential arbitrary command execution or recursive deletion"
152152
}
153153
];
154+
var SOFT_DENY_RULES = [
155+
{ pattern: /^git\s+push\s+--force(?:-with-lease)?\b/i, reason: "Force push needs user intent verification" },
156+
{ pattern: /^git\s+push\s+.*\s-(?!-)\S*f/i, reason: "Force push (short flag) needs user intent verification" },
157+
{ pattern: /^git\s+push\s+(?:.*\s)?(?:origin\s+)?(main|master)\s*$/i, reason: "Push to default branch needs user intent verification" },
158+
{ pattern: /^git\s+reset\s+--hard\b/i, reason: "Hard reset needs user intent verification" },
159+
{ pattern: /^git\s+clean\s+-[a-z]*f/i, reason: "Git clean needs user intent verification" },
160+
{ pattern: /^git\s+branch\s+-[a-zA-Z]*D/i, reason: "Force branch delete needs user intent verification" },
161+
{ pattern: /^npm\s+publish\b/i, reason: "Package publish needs user intent verification" },
162+
{ pattern: /^(terraform|pulumi)\s+apply\b/i, reason: "Infrastructure apply needs user intent verification" },
163+
{ pattern: /^(terraform|pulumi)\s+destroy\b/i, reason: "Infrastructure destroy needs user intent verification" },
164+
{ pattern: /^kubectl\s+(apply|delete)\b/i, reason: "Kubernetes mutation needs user intent verification" },
165+
{ pattern: /\b(\.claude\/settings|CLAUDE\.md)\b/i, reason: "Agent self-modification needs user intent verification" },
166+
{ pattern: /^git\s+commit\s+.*--no-verify\b/i, reason: "Skipping commit verification needs user intent verification" },
167+
{ pattern: /\bchmod\s+777\b/i, reason: "Broad permission change needs user intent verification" },
168+
{ pattern: /\b(nc|ncat|socat)\s+-l/i, reason: "Exposing local service needs user intent verification" },
169+
{ pattern: /\bpython3?\s+-m\s+http\.server/i, reason: "Exposing HTTP server needs user intent verification" },
170+
{ pattern: /\b(crontab|systemctl\s+enable|ssh-keygen|ssh-copy-id)\b/i, reason: "Unauthorized persistence needs user intent verification" },
171+
{ pattern: /\b(gcloud\s+.*add-iam|aws\s+iam|az\s+role\s+assignment)\b/i, reason: "Permission grant needs user intent verification" },
172+
{ pattern: /\bsystemctl\s+stop\s+.*log/i, reason: "Logging tampering needs user intent verification" }
173+
];
154174
var ALLOW_RULES = [
155175
{
156176
pattern: /^(npm|yarn|pnpm|bun)\s+(test|run|install|ci|add|remove|ls|info|outdated|audit|why)\b/i,
@@ -177,6 +197,63 @@ var ALLOW_RULES = [
177197
reason: "Safe docker read operation"
178198
}
179199
];
200+
var WRITE_EDIT_SOFT_DENY_PATTERNS = [
201+
{ pattern: /(?:^|[/\\])\.env(?:\.|$)/i, reason: "Writing to .env file needs user intent verification" },
202+
{ pattern: /(?:^|[/\\])\.claude[/\\]settings/i, reason: "Writing to .claude/settings needs user intent verification" },
203+
{ pattern: /(?:^|[/\\])CLAUDE\.md$/i, reason: "Writing to CLAUDE.md needs user intent verification" },
204+
{ pattern: /(?:^|[/\\])\.github[/\\]workflows[/\\]/i, reason: "Writing to CI/CD config needs user intent verification" },
205+
{ pattern: /(?:^|[/\\])\.gitlab-ci\.yml$/i, reason: "Writing to CI/CD config needs user intent verification" },
206+
{ pattern: /(?:^|[/\\])Jenkinsfile$/i, reason: "Writing to CI/CD config needs user intent verification" },
207+
{ pattern: /(?:^|[/\\])\.circleci[/\\]/i, reason: "Writing to CI/CD config needs user intent verification" }
208+
];
209+
function classifyWriteEdit(filePath) {
210+
if (!filePath) {
211+
return null;
212+
}
213+
for (const rule of WRITE_EDIT_SOFT_DENY_PATTERNS) {
214+
if (rule.pattern.test(filePath)) {
215+
return { decision: "soft_deny", reason: rule.reason };
216+
}
217+
}
218+
if (!filePath.startsWith("/") || filePath.includes("/node_modules/")) {
219+
return { decision: "allow", reason: "Safe project file write" };
220+
}
221+
return null;
222+
}
223+
var WEBFETCH_SOFT_DENY_PATTERNS = [
224+
{ pattern: /\b(pastebin\.com|paste\.ee|hastebin\.com|dpaste\.org|ghostbin\.com|rentry\.co)\b/i, reason: "Paste service needs user intent verification" },
225+
{ pattern: /\b(transfer\.sh|file\.io|0x0\.st|tmpfiles\.org)\b/i, reason: "File sharing service needs user intent verification" },
226+
{ pattern: /\.(sh|bash|ps1|bat|cmd)(\?|$)/i, reason: "Script download needs user intent verification" },
227+
{ pattern: /\braw\.githubusercontent\.com\/.*\.(sh|py|rb|js)$/i, reason: "Raw script download needs user intent verification" }
228+
];
229+
function classifyWebFetch(url) {
230+
if (!url) {
231+
return null;
232+
}
233+
for (const rule of WEBFETCH_SOFT_DENY_PATTERNS) {
234+
if (rule.pattern.test(url)) {
235+
return { decision: "soft_deny", reason: rule.reason };
236+
}
237+
}
238+
if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?/i.test(url)) {
239+
return { decision: "allow", reason: "Safe localhost request" };
240+
}
241+
return null;
242+
}
243+
var SAFE_TOOLS = new Set([
244+
"Read",
245+
"Glob",
246+
"Grep",
247+
"LS",
248+
"Search",
249+
"TaskCreate",
250+
"TaskUpdate",
251+
"TaskList",
252+
"TaskGet",
253+
"TodoRead",
254+
"TodoWrite",
255+
"NotebookRead"
256+
]);
180257
function isGitPushNonForce(cmd) {
181258
return /^git\s+push\b/i.test(cmd) && !/--force(?:-with-lease)?\b|\s-(?!-)\S*f/i.test(cmd);
182259
}
@@ -197,9 +274,14 @@ function evaluateSingleCommand(cmd) {
197274
if (!cmd.trim()) {
198275
return null;
199276
}
200-
for (const rule of DENY_RULES) {
277+
for (const rule of HARD_DENY_RULES) {
201278
if (rule.pattern.test(cmd)) {
202-
return { decision: "deny", reason: rule.reason };
279+
return { decision: "hard_deny", reason: rule.reason };
280+
}
281+
}
282+
for (const rule of SOFT_DENY_RULES) {
283+
if (rule.pattern.test(cmd)) {
284+
return { decision: "soft_deny", reason: rule.reason };
203285
}
204286
}
205287
for (const rule of ALLOW_RULES) {
@@ -212,15 +294,27 @@ function evaluateSingleCommand(cmd) {
212294
}
213295
return null;
214296
}
215-
function evaluate(input) {
216-
if (input.tool_name !== "Bash") {
217-
return null;
297+
function decisionToOutput(decision, reason, label) {
298+
switch (decision) {
299+
case "hard_deny":
300+
process.stderr.write(`gatekeeper: deny "${label}" — ${reason}
301+
`);
302+
return makeDecision("deny", reason);
303+
case "soft_deny":
304+
process.stderr.write(`gatekeeper: soft_deny "${label}" — ${reason}
305+
`);
306+
return null;
307+
case "allow":
308+
process.stderr.write(`gatekeeper: allow "${label}" — ${reason}
309+
`);
310+
return makeDecision("allow", reason);
218311
}
219-
const cmd = (input.tool_input?.command ?? "").trim();
312+
}
313+
function evaluateBash(cmd) {
220314
if (!cmd) {
221315
return null;
222316
}
223-
for (const rule of DENY_RULES) {
317+
for (const rule of HARD_DENY_RULES) {
224318
if (rule.pattern.test(cmd)) {
225319
process.stderr.write(`gatekeeper: deny "${cmd}" — ${rule.reason}
226320
`);
@@ -236,9 +330,7 @@ function evaluate(input) {
236330
if (parsed.kind === "single") {
237331
const result = evaluateSingleCommand(cmd);
238332
if (result) {
239-
process.stderr.write(`gatekeeper: ${result.decision} "${cmd}" — ${result.reason}
240-
`);
241-
return makeDecision(result.decision, result.reason);
333+
return decisionToOutput(result.decision, result.reason, cmd);
242334
}
243335
process.stderr.write(`gatekeeper: passthrough "${cmd}" — no matching rule
244336
`);
@@ -248,13 +340,13 @@ function evaluate(input) {
248340
let firstAllowedResult = null;
249341
for (const part of parsed.parts) {
250342
const result = evaluateSingleCommand(part);
251-
if (result?.decision === "deny") {
343+
if (result?.decision === "hard_deny") {
252344
process.stderr.write(`gatekeeper: deny "${cmd}" — part "${part}": ${result.reason}
253345
`);
254346
return makeDecision("deny", result.reason);
255347
}
256-
if (result === null) {
257-
process.stderr.write(`gatekeeper: passthrough "${cmd}" — unknown part "${part}"
348+
if (result?.decision === "soft_deny" || result === null) {
349+
process.stderr.write(`gatekeeper: passthrough "${cmd}" — ${result ? "soft_deny" : "unknown"} part "${part}"
258350
`);
259351
return null;
260352
}
@@ -273,6 +365,42 @@ function evaluate(input) {
273365
`);
274366
return makeDecision("allow", compositeReason);
275367
}
368+
function evaluate(input) {
369+
const toolName = input.tool_name;
370+
const toolInput = input.tool_input;
371+
if (SAFE_TOOLS.has(toolName)) {
372+
process.stderr.write(`gatekeeper: allow "${toolName}" — safe tool
373+
`);
374+
return makeDecision("allow", `Safe tool: ${toolName}`);
375+
}
376+
if (toolName === "Bash") {
377+
const cmd = (toolInput?.command ?? "").trim();
378+
return evaluateBash(cmd);
379+
}
380+
if (toolName === "Write" || toolName === "Edit") {
381+
const filePath = toolInput?.file_path ?? "";
382+
const result = classifyWriteEdit(filePath);
383+
if (result) {
384+
return decisionToOutput(result.decision, result.reason, `${toolName}:${filePath}`);
385+
}
386+
process.stderr.write(`gatekeeper: passthrough "${toolName}:${filePath}" — no matching rule
387+
`);
388+
return null;
389+
}
390+
if (toolName === "WebFetch") {
391+
const url = toolInput?.url ?? "";
392+
const result = classifyWebFetch(url);
393+
if (result) {
394+
return decisionToOutput(result.decision, result.reason, `WebFetch:${url}`);
395+
}
396+
process.stderr.write(`gatekeeper: passthrough "WebFetch:${url}" — no matching rule
397+
`);
398+
return null;
399+
}
400+
process.stderr.write(`gatekeeper: passthrough "${toolName}" — unknown tool
401+
`);
402+
return null;
403+
}
276404
function readStdin() {
277405
return new Promise((resolve, reject) => {
278406
let data = "";
@@ -320,6 +448,9 @@ export {
320448
isGitPushNonForce,
321449
evaluateSingleCommand,
322450
evaluate,
323-
DENY_RULES,
451+
classifyWriteEdit,
452+
classifyWebFetch,
453+
SOFT_DENY_RULES,
454+
HARD_DENY_RULES,
324455
ALLOW_RULES
325456
};

0 commit comments

Comments
 (0)