@node9/proxy 1.7.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -21
- package/dist/cli.js +211 -265
- package/dist/cli.mjs +211 -265
- package/dist/index.js +89 -257
- package/dist/index.mjs +89 -257
- package/dist/shields/builtin/aws.json +59 -0
- package/dist/shields/builtin/bash-safe.json +78 -0
- package/dist/shields/builtin/filesystem.json +30 -0
- package/dist/shields/builtin/github.json +26 -0
- package/dist/shields/builtin/postgres.json +42 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -280,251 +280,73 @@ ${lines.join("\n")}`
|
|
|
280
280
|
var import_fs2 = __toESM(require("fs"));
|
|
281
281
|
var import_path2 = __toESM(require("path"));
|
|
282
282
|
var import_os2 = __toESM(require("os"));
|
|
283
|
-
var
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
name: "shield:postgres:block-drop-table",
|
|
291
|
-
tool: "*",
|
|
292
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
293
|
-
verdict: "block",
|
|
294
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
name: "shield:postgres:block-truncate",
|
|
298
|
-
tool: "*",
|
|
299
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
300
|
-
verdict: "block",
|
|
301
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
name: "shield:postgres:block-drop-column",
|
|
305
|
-
tool: "*",
|
|
306
|
-
conditions: [
|
|
307
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
308
|
-
],
|
|
309
|
-
verdict: "block",
|
|
310
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
name: "shield:postgres:review-grant-revoke",
|
|
314
|
-
tool: "*",
|
|
315
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
316
|
-
verdict: "review",
|
|
317
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
318
|
-
}
|
|
319
|
-
],
|
|
320
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
321
|
-
},
|
|
322
|
-
github: {
|
|
323
|
-
name: "github",
|
|
324
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
325
|
-
aliases: ["git"],
|
|
326
|
-
smartRules: [
|
|
327
|
-
{
|
|
328
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
329
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
330
|
-
name: "shield:github:review-delete-branch-remote",
|
|
331
|
-
tool: "bash",
|
|
332
|
-
conditions: [
|
|
333
|
-
{
|
|
334
|
-
field: "command",
|
|
335
|
-
op: "matches",
|
|
336
|
-
value: "git\\s+push\\s+.*--delete",
|
|
337
|
-
flags: "i"
|
|
338
|
-
}
|
|
339
|
-
],
|
|
340
|
-
verdict: "review",
|
|
341
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
name: "shield:github:block-delete-repo",
|
|
345
|
-
tool: "*",
|
|
346
|
-
conditions: [
|
|
347
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
348
|
-
],
|
|
349
|
-
verdict: "block",
|
|
350
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
351
|
-
}
|
|
352
|
-
],
|
|
353
|
-
dangerousWords: []
|
|
354
|
-
},
|
|
355
|
-
aws: {
|
|
356
|
-
name: "aws",
|
|
357
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
358
|
-
aliases: ["amazon"],
|
|
359
|
-
smartRules: [
|
|
360
|
-
{
|
|
361
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
362
|
-
tool: "*",
|
|
363
|
-
conditions: [
|
|
364
|
-
{
|
|
365
|
-
field: "command",
|
|
366
|
-
op: "matches",
|
|
367
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
368
|
-
flags: "i"
|
|
369
|
-
}
|
|
370
|
-
],
|
|
371
|
-
verdict: "block",
|
|
372
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
name: "shield:aws:review-iam-changes",
|
|
376
|
-
tool: "*",
|
|
377
|
-
conditions: [
|
|
378
|
-
{
|
|
379
|
-
field: "command",
|
|
380
|
-
op: "matches",
|
|
381
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
382
|
-
flags: "i"
|
|
383
|
-
}
|
|
384
|
-
],
|
|
385
|
-
verdict: "review",
|
|
386
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
name: "shield:aws:block-ec2-terminate",
|
|
390
|
-
tool: "*",
|
|
391
|
-
conditions: [
|
|
392
|
-
{
|
|
393
|
-
field: "command",
|
|
394
|
-
op: "matches",
|
|
395
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
396
|
-
flags: "i"
|
|
397
|
-
}
|
|
398
|
-
],
|
|
399
|
-
verdict: "block",
|
|
400
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
name: "shield:aws:review-rds-delete",
|
|
404
|
-
tool: "*",
|
|
405
|
-
conditions: [
|
|
406
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
407
|
-
],
|
|
408
|
-
verdict: "review",
|
|
409
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
410
|
-
}
|
|
411
|
-
],
|
|
412
|
-
dangerousWords: []
|
|
413
|
-
},
|
|
414
|
-
"bash-safe": {
|
|
415
|
-
name: "bash-safe",
|
|
416
|
-
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
417
|
-
aliases: ["bash", "shell"],
|
|
418
|
-
smartRules: [
|
|
419
|
-
{
|
|
420
|
-
name: "shield:bash-safe:block-pipe-to-shell",
|
|
421
|
-
tool: "bash",
|
|
422
|
-
conditions: [
|
|
423
|
-
{
|
|
424
|
-
field: "command",
|
|
425
|
-
op: "matches",
|
|
426
|
-
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
427
|
-
flags: "i"
|
|
428
|
-
}
|
|
429
|
-
],
|
|
430
|
-
verdict: "block",
|
|
431
|
-
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
name: "shield:bash-safe:block-obfuscated-exec",
|
|
435
|
-
tool: "bash",
|
|
436
|
-
conditions: [
|
|
437
|
-
{
|
|
438
|
-
field: "command",
|
|
439
|
-
op: "matches",
|
|
440
|
-
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
441
|
-
flags: "i"
|
|
442
|
-
}
|
|
443
|
-
],
|
|
444
|
-
verdict: "block",
|
|
445
|
-
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
name: "shield:bash-safe:block-rm-root",
|
|
449
|
-
tool: "bash",
|
|
450
|
-
conditions: [
|
|
451
|
-
{
|
|
452
|
-
field: "command",
|
|
453
|
-
op: "matches",
|
|
454
|
-
value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
|
|
455
|
-
flags: "i"
|
|
456
|
-
}
|
|
457
|
-
],
|
|
458
|
-
verdict: "block",
|
|
459
|
-
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
name: "shield:bash-safe:block-disk-overwrite",
|
|
463
|
-
tool: "bash",
|
|
464
|
-
conditions: [
|
|
465
|
-
{
|
|
466
|
-
field: "command",
|
|
467
|
-
op: "matches",
|
|
468
|
-
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
469
|
-
flags: "i"
|
|
470
|
-
}
|
|
471
|
-
],
|
|
472
|
-
verdict: "block",
|
|
473
|
-
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
name: "shield:bash-safe:review-eval",
|
|
477
|
-
tool: "bash",
|
|
478
|
-
conditions: [
|
|
479
|
-
{
|
|
480
|
-
field: "command",
|
|
481
|
-
op: "matches",
|
|
482
|
-
value: '\\beval\\s+[\\$`("]',
|
|
483
|
-
flags: "i"
|
|
484
|
-
}
|
|
485
|
-
],
|
|
486
|
-
verdict: "review",
|
|
487
|
-
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
488
|
-
}
|
|
489
|
-
],
|
|
490
|
-
dangerousWords: []
|
|
491
|
-
},
|
|
492
|
-
filesystem: {
|
|
493
|
-
name: "filesystem",
|
|
494
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
495
|
-
aliases: ["fs"],
|
|
496
|
-
smartRules: [
|
|
497
|
-
{
|
|
498
|
-
name: "shield:filesystem:review-chmod-777",
|
|
499
|
-
tool: "bash",
|
|
500
|
-
conditions: [
|
|
501
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
502
|
-
],
|
|
503
|
-
verdict: "review",
|
|
504
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
name: "shield:filesystem:review-write-etc",
|
|
508
|
-
tool: "bash",
|
|
509
|
-
conditions: [
|
|
510
|
-
{
|
|
511
|
-
field: "command",
|
|
512
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
513
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
514
|
-
op: "matches",
|
|
515
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
516
|
-
}
|
|
517
|
-
],
|
|
518
|
-
verdict: "review",
|
|
519
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
520
|
-
}
|
|
521
|
-
],
|
|
522
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
523
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
524
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
525
|
-
dangerousWords: ["wipefs"]
|
|
283
|
+
var BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
|
|
284
|
+
var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
|
|
285
|
+
function validateShieldDefinition(raw, filePath) {
|
|
286
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
287
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
288
|
+
`);
|
|
289
|
+
return null;
|
|
526
290
|
}
|
|
527
|
-
|
|
291
|
+
const r = raw;
|
|
292
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
293
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
294
|
+
`);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (typeof r.description !== "string") {
|
|
298
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
299
|
+
`);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (!Array.isArray(r.aliases)) {
|
|
303
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
304
|
+
`);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
if (!Array.isArray(r.smartRules)) {
|
|
308
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
309
|
+
`);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
313
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
314
|
+
`);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return r;
|
|
318
|
+
}
|
|
319
|
+
function loadShieldsFromDir(dir, label) {
|
|
320
|
+
const result = {};
|
|
321
|
+
let entries;
|
|
322
|
+
try {
|
|
323
|
+
entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err.code !== "ENOENT") {
|
|
326
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err)}
|
|
327
|
+
`);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
for (const file of entries) {
|
|
332
|
+
const filePath = import_path2.default.join(dir, file);
|
|
333
|
+
try {
|
|
334
|
+
const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
335
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
336
|
+
if (shield) result[shield.name] = shield;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err)}
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
function buildSHIELDS() {
|
|
345
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
346
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
347
|
+
return { ...builtins, ...userShields };
|
|
348
|
+
}
|
|
349
|
+
var SHIELDS = buildSHIELDS();
|
|
528
350
|
function resolveShieldName(input) {
|
|
529
351
|
const lower = input.toLowerCase();
|
|
530
352
|
if (SHIELDS[lower]) return lower;
|
|
@@ -2148,7 +1970,7 @@ function isDaemonRunning() {
|
|
|
2148
1970
|
return false;
|
|
2149
1971
|
}
|
|
2150
1972
|
}
|
|
2151
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
1973
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2152
1974
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2153
1975
|
const ctrl = new AbortController();
|
|
2154
1976
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2169,7 +1991,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2169
1991
|
...cwd && { cwd },
|
|
2170
1992
|
...recoveryCommand && { recoveryCommand },
|
|
2171
1993
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2172
|
-
...viewOnly && { viewOnly: true }
|
|
1994
|
+
...viewOnly && { viewOnly: true },
|
|
1995
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2173
1996
|
}),
|
|
2174
1997
|
signal: ctrl.signal
|
|
2175
1998
|
});
|
|
@@ -2837,6 +2660,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2837
2660
|
let policyMatchedWord;
|
|
2838
2661
|
let riskMetadata;
|
|
2839
2662
|
let statefulRecoveryCommand;
|
|
2663
|
+
let localSmartRuleMatched = false;
|
|
2840
2664
|
let taintWarning = null;
|
|
2841
2665
|
if (isNetworkTool(toolName, args)) {
|
|
2842
2666
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2980,6 +2804,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2980
2804
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2981
2805
|
policyMatchedField = policyResult.matchedField;
|
|
2982
2806
|
policyMatchedWord = policyResult.matchedWord;
|
|
2807
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
2983
2808
|
riskMetadata = computeRiskMetadata(
|
|
2984
2809
|
args,
|
|
2985
2810
|
policyResult.tier ?? 6,
|
|
@@ -3022,22 +2847,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3022
2847
|
}
|
|
3023
2848
|
let cloudRequestId = null;
|
|
3024
2849
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3025
|
-
if (cloudEnforced) {
|
|
2850
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3026
2851
|
try {
|
|
3027
2852
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3028
2853
|
if (!initResult.pending) {
|
|
3029
2854
|
if (initResult.shadowMode) {
|
|
3030
2855
|
return { approved: true, checkedBy: "cloud" };
|
|
3031
2856
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
2857
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2858
|
+
return {
|
|
2859
|
+
approved: !!initResult.approved,
|
|
2860
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
2861
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
2862
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
2863
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2868
|
+
cloudRequestId = initResult.requestId || null;
|
|
3039
2869
|
}
|
|
3040
|
-
cloudRequestId = initResult.requestId || null;
|
|
3041
2870
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3042
2871
|
} catch {
|
|
3043
2872
|
}
|
|
@@ -3083,7 +2912,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3083
2912
|
riskMetadata,
|
|
3084
2913
|
options?.activityId,
|
|
3085
2914
|
options?.cwd,
|
|
3086
|
-
statefulRecoveryCommand
|
|
2915
|
+
statefulRecoveryCommand,
|
|
2916
|
+
void 0,
|
|
2917
|
+
void 0,
|
|
2918
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3087
2919
|
);
|
|
3088
2920
|
daemonEntryId = entry.id;
|
|
3089
2921
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3091,7 +2923,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3091
2923
|
}
|
|
3092
2924
|
}
|
|
3093
2925
|
}
|
|
3094
|
-
if (cloudEnforced && cloudRequestId) {
|
|
2926
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3095
2927
|
racePromises.push(
|
|
3096
2928
|
(async () => {
|
|
3097
2929
|
try {
|