@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/cli.js
CHANGED
|
@@ -268,6 +268,70 @@ var init_config_schema = __esm({
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
// src/shields.ts
|
|
271
|
+
function validateShieldDefinition(raw, filePath) {
|
|
272
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
273
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
274
|
+
`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const r = raw;
|
|
278
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
279
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
280
|
+
`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if (typeof r.description !== "string") {
|
|
284
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
285
|
+
`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
if (!Array.isArray(r.aliases)) {
|
|
289
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
290
|
+
`);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(r.smartRules)) {
|
|
294
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
295
|
+
`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
299
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
300
|
+
`);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return r;
|
|
304
|
+
}
|
|
305
|
+
function loadShieldsFromDir(dir, label) {
|
|
306
|
+
const result = {};
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
310
|
+
} catch (err2) {
|
|
311
|
+
if (err2.code !== "ENOENT") {
|
|
312
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
for (const file of entries) {
|
|
318
|
+
const filePath = import_path2.default.join(dir, file);
|
|
319
|
+
try {
|
|
320
|
+
const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
321
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
322
|
+
if (shield) result[shield.name] = shield;
|
|
323
|
+
} catch (err2) {
|
|
324
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
function buildSHIELDS() {
|
|
331
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
332
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
333
|
+
return { ...builtins, ...userShields };
|
|
334
|
+
}
|
|
271
335
|
function resolveShieldName(input) {
|
|
272
336
|
const lower = input.toLowerCase();
|
|
273
337
|
if (SHIELDS[lower]) return lower;
|
|
@@ -374,7 +438,24 @@ function resolveShieldRule(shieldName, identifier) {
|
|
|
374
438
|
}
|
|
375
439
|
return null;
|
|
376
440
|
}
|
|
377
|
-
|
|
441
|
+
function installShield(name, shieldJson) {
|
|
442
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
|
|
448
|
+
if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
|
|
449
|
+
if (shield.name !== name) {
|
|
450
|
+
throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
|
|
451
|
+
}
|
|
452
|
+
import_fs2.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
|
|
453
|
+
const filePath = import_path2.default.join(USER_SHIELDS_DIR, `${name}.json`);
|
|
454
|
+
const tmp = `${filePath}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
|
|
455
|
+
import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
|
|
456
|
+
import_fs2.default.renameSync(tmp, filePath);
|
|
457
|
+
}
|
|
458
|
+
var import_fs2, import_path2, import_os2, import_crypto2, BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
|
|
378
459
|
var init_shields = __esm({
|
|
379
460
|
"src/shields.ts"() {
|
|
380
461
|
"use strict";
|
|
@@ -382,251 +463,9 @@ var init_shields = __esm({
|
|
|
382
463
|
import_path2 = __toESM(require("path"));
|
|
383
464
|
import_os2 = __toESM(require("os"));
|
|
384
465
|
import_crypto2 = __toESM(require("crypto"));
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
389
|
-
aliases: ["pg", "postgresql"],
|
|
390
|
-
smartRules: [
|
|
391
|
-
{
|
|
392
|
-
name: "shield:postgres:block-drop-table",
|
|
393
|
-
tool: "*",
|
|
394
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
395
|
-
verdict: "block",
|
|
396
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
name: "shield:postgres:block-truncate",
|
|
400
|
-
tool: "*",
|
|
401
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
402
|
-
verdict: "block",
|
|
403
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "shield:postgres:block-drop-column",
|
|
407
|
-
tool: "*",
|
|
408
|
-
conditions: [
|
|
409
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
410
|
-
],
|
|
411
|
-
verdict: "block",
|
|
412
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
name: "shield:postgres:review-grant-revoke",
|
|
416
|
-
tool: "*",
|
|
417
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
418
|
-
verdict: "review",
|
|
419
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
420
|
-
}
|
|
421
|
-
],
|
|
422
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
423
|
-
},
|
|
424
|
-
github: {
|
|
425
|
-
name: "github",
|
|
426
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
427
|
-
aliases: ["git"],
|
|
428
|
-
smartRules: [
|
|
429
|
-
{
|
|
430
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
431
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
432
|
-
name: "shield:github:review-delete-branch-remote",
|
|
433
|
-
tool: "bash",
|
|
434
|
-
conditions: [
|
|
435
|
-
{
|
|
436
|
-
field: "command",
|
|
437
|
-
op: "matches",
|
|
438
|
-
value: "git\\s+push\\s+.*--delete",
|
|
439
|
-
flags: "i"
|
|
440
|
-
}
|
|
441
|
-
],
|
|
442
|
-
verdict: "review",
|
|
443
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
name: "shield:github:block-delete-repo",
|
|
447
|
-
tool: "*",
|
|
448
|
-
conditions: [
|
|
449
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
450
|
-
],
|
|
451
|
-
verdict: "block",
|
|
452
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
453
|
-
}
|
|
454
|
-
],
|
|
455
|
-
dangerousWords: []
|
|
456
|
-
},
|
|
457
|
-
aws: {
|
|
458
|
-
name: "aws",
|
|
459
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
460
|
-
aliases: ["amazon"],
|
|
461
|
-
smartRules: [
|
|
462
|
-
{
|
|
463
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
464
|
-
tool: "*",
|
|
465
|
-
conditions: [
|
|
466
|
-
{
|
|
467
|
-
field: "command",
|
|
468
|
-
op: "matches",
|
|
469
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
470
|
-
flags: "i"
|
|
471
|
-
}
|
|
472
|
-
],
|
|
473
|
-
verdict: "block",
|
|
474
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
name: "shield:aws:review-iam-changes",
|
|
478
|
-
tool: "*",
|
|
479
|
-
conditions: [
|
|
480
|
-
{
|
|
481
|
-
field: "command",
|
|
482
|
-
op: "matches",
|
|
483
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
484
|
-
flags: "i"
|
|
485
|
-
}
|
|
486
|
-
],
|
|
487
|
-
verdict: "review",
|
|
488
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
name: "shield:aws:block-ec2-terminate",
|
|
492
|
-
tool: "*",
|
|
493
|
-
conditions: [
|
|
494
|
-
{
|
|
495
|
-
field: "command",
|
|
496
|
-
op: "matches",
|
|
497
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
498
|
-
flags: "i"
|
|
499
|
-
}
|
|
500
|
-
],
|
|
501
|
-
verdict: "block",
|
|
502
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
name: "shield:aws:review-rds-delete",
|
|
506
|
-
tool: "*",
|
|
507
|
-
conditions: [
|
|
508
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
509
|
-
],
|
|
510
|
-
verdict: "review",
|
|
511
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
512
|
-
}
|
|
513
|
-
],
|
|
514
|
-
dangerousWords: []
|
|
515
|
-
},
|
|
516
|
-
"bash-safe": {
|
|
517
|
-
name: "bash-safe",
|
|
518
|
-
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
519
|
-
aliases: ["bash", "shell"],
|
|
520
|
-
smartRules: [
|
|
521
|
-
{
|
|
522
|
-
name: "shield:bash-safe:block-pipe-to-shell",
|
|
523
|
-
tool: "bash",
|
|
524
|
-
conditions: [
|
|
525
|
-
{
|
|
526
|
-
field: "command",
|
|
527
|
-
op: "matches",
|
|
528
|
-
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
529
|
-
flags: "i"
|
|
530
|
-
}
|
|
531
|
-
],
|
|
532
|
-
verdict: "block",
|
|
533
|
-
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
name: "shield:bash-safe:block-obfuscated-exec",
|
|
537
|
-
tool: "bash",
|
|
538
|
-
conditions: [
|
|
539
|
-
{
|
|
540
|
-
field: "command",
|
|
541
|
-
op: "matches",
|
|
542
|
-
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
543
|
-
flags: "i"
|
|
544
|
-
}
|
|
545
|
-
],
|
|
546
|
-
verdict: "block",
|
|
547
|
-
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
name: "shield:bash-safe:block-rm-root",
|
|
551
|
-
tool: "bash",
|
|
552
|
-
conditions: [
|
|
553
|
-
{
|
|
554
|
-
field: "command",
|
|
555
|
-
op: "matches",
|
|
556
|
-
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*$",
|
|
557
|
-
flags: "i"
|
|
558
|
-
}
|
|
559
|
-
],
|
|
560
|
-
verdict: "block",
|
|
561
|
-
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
name: "shield:bash-safe:block-disk-overwrite",
|
|
565
|
-
tool: "bash",
|
|
566
|
-
conditions: [
|
|
567
|
-
{
|
|
568
|
-
field: "command",
|
|
569
|
-
op: "matches",
|
|
570
|
-
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
571
|
-
flags: "i"
|
|
572
|
-
}
|
|
573
|
-
],
|
|
574
|
-
verdict: "block",
|
|
575
|
-
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
name: "shield:bash-safe:review-eval",
|
|
579
|
-
tool: "bash",
|
|
580
|
-
conditions: [
|
|
581
|
-
{
|
|
582
|
-
field: "command",
|
|
583
|
-
op: "matches",
|
|
584
|
-
value: '\\beval\\s+[\\$`("]',
|
|
585
|
-
flags: "i"
|
|
586
|
-
}
|
|
587
|
-
],
|
|
588
|
-
verdict: "review",
|
|
589
|
-
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
590
|
-
}
|
|
591
|
-
],
|
|
592
|
-
dangerousWords: []
|
|
593
|
-
},
|
|
594
|
-
filesystem: {
|
|
595
|
-
name: "filesystem",
|
|
596
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
597
|
-
aliases: ["fs"],
|
|
598
|
-
smartRules: [
|
|
599
|
-
{
|
|
600
|
-
name: "shield:filesystem:review-chmod-777",
|
|
601
|
-
tool: "bash",
|
|
602
|
-
conditions: [
|
|
603
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
604
|
-
],
|
|
605
|
-
verdict: "review",
|
|
606
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
name: "shield:filesystem:review-write-etc",
|
|
610
|
-
tool: "bash",
|
|
611
|
-
conditions: [
|
|
612
|
-
{
|
|
613
|
-
field: "command",
|
|
614
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
615
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
616
|
-
op: "matches",
|
|
617
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
618
|
-
}
|
|
619
|
-
],
|
|
620
|
-
verdict: "review",
|
|
621
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
622
|
-
}
|
|
623
|
-
],
|
|
624
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
625
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
626
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
627
|
-
dangerousWords: ["wipefs"]
|
|
628
|
-
}
|
|
629
|
-
};
|
|
466
|
+
BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
|
|
467
|
+
USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
|
|
468
|
+
SHIELDS = buildSHIELDS();
|
|
630
469
|
SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
|
|
631
470
|
}
|
|
632
471
|
});
|
|
@@ -2598,7 +2437,7 @@ function isDaemonRunning() {
|
|
|
2598
2437
|
return false;
|
|
2599
2438
|
}
|
|
2600
2439
|
}
|
|
2601
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2440
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2602
2441
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2603
2442
|
const ctrl = new AbortController();
|
|
2604
2443
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2619,7 +2458,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2619
2458
|
...cwd && { cwd },
|
|
2620
2459
|
...recoveryCommand && { recoveryCommand },
|
|
2621
2460
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2622
|
-
...viewOnly && { viewOnly: true }
|
|
2461
|
+
...viewOnly && { viewOnly: true },
|
|
2462
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2623
2463
|
}),
|
|
2624
2464
|
signal: ctrl.signal
|
|
2625
2465
|
});
|
|
@@ -3314,6 +3154,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3314
3154
|
let policyMatchedWord;
|
|
3315
3155
|
let riskMetadata;
|
|
3316
3156
|
let statefulRecoveryCommand;
|
|
3157
|
+
let localSmartRuleMatched = false;
|
|
3317
3158
|
let taintWarning = null;
|
|
3318
3159
|
if (isNetworkTool(toolName, args)) {
|
|
3319
3160
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3457,6 +3298,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3457
3298
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3458
3299
|
policyMatchedField = policyResult.matchedField;
|
|
3459
3300
|
policyMatchedWord = policyResult.matchedWord;
|
|
3301
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3460
3302
|
riskMetadata = computeRiskMetadata(
|
|
3461
3303
|
args,
|
|
3462
3304
|
policyResult.tier ?? 6,
|
|
@@ -3499,22 +3341,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3499
3341
|
}
|
|
3500
3342
|
let cloudRequestId = null;
|
|
3501
3343
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3502
|
-
if (cloudEnforced) {
|
|
3344
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3503
3345
|
try {
|
|
3504
3346
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3505
3347
|
if (!initResult.pending) {
|
|
3506
3348
|
if (initResult.shadowMode) {
|
|
3507
3349
|
return { approved: true, checkedBy: "cloud" };
|
|
3508
3350
|
}
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3351
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3352
|
+
return {
|
|
3353
|
+
approved: !!initResult.approved,
|
|
3354
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
3355
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
3356
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
3357
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3362
|
+
cloudRequestId = initResult.requestId || null;
|
|
3516
3363
|
}
|
|
3517
|
-
cloudRequestId = initResult.requestId || null;
|
|
3518
3364
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3519
3365
|
} catch {
|
|
3520
3366
|
}
|
|
@@ -3560,7 +3406,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3560
3406
|
riskMetadata,
|
|
3561
3407
|
options?.activityId,
|
|
3562
3408
|
options?.cwd,
|
|
3563
|
-
statefulRecoveryCommand
|
|
3409
|
+
statefulRecoveryCommand,
|
|
3410
|
+
void 0,
|
|
3411
|
+
void 0,
|
|
3412
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3564
3413
|
);
|
|
3565
3414
|
daemonEntryId = entry.id;
|
|
3566
3415
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3568,7 +3417,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3568
3417
|
}
|
|
3569
3418
|
}
|
|
3570
3419
|
}
|
|
3571
|
-
if (cloudEnforced && cloudRequestId) {
|
|
3420
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3572
3421
|
racePromises.push(
|
|
3573
3422
|
(async () => {
|
|
3574
3423
|
try {
|
|
@@ -6230,7 +6079,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6230
6079
|
viewOnly = false,
|
|
6231
6080
|
fromCLI = false,
|
|
6232
6081
|
activityId,
|
|
6233
|
-
cwd
|
|
6082
|
+
cwd,
|
|
6083
|
+
localSmartRuleMatched = false
|
|
6234
6084
|
} = JSON.parse(body);
|
|
6235
6085
|
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
|
|
6236
6086
|
const entry = {
|
|
@@ -6310,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6310
6160
|
agent: typeof agent === "string" ? agent : void 0,
|
|
6311
6161
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
6312
6162
|
},
|
|
6313
|
-
{ calledFromDaemon: true }
|
|
6163
|
+
{ calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
|
|
6314
6164
|
).then((result) => {
|
|
6315
6165
|
const e = pending.get(id);
|
|
6316
6166
|
if (!e) return;
|
|
@@ -9161,7 +9011,7 @@ RAW: ${raw}
|
|
|
9161
9011
|
}
|
|
9162
9012
|
}) + "\n"
|
|
9163
9013
|
);
|
|
9164
|
-
process.exit(
|
|
9014
|
+
process.exit(2);
|
|
9165
9015
|
};
|
|
9166
9016
|
if (!toolName) {
|
|
9167
9017
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
@@ -9396,6 +9246,27 @@ var import_chalk6 = __toESM(require("chalk"));
|
|
|
9396
9246
|
init_shields();
|
|
9397
9247
|
init_audit();
|
|
9398
9248
|
init_config();
|
|
9249
|
+
|
|
9250
|
+
// src/utils/https-fetch.ts
|
|
9251
|
+
var import_https = __toESM(require("https"));
|
|
9252
|
+
function httpsFetch(url) {
|
|
9253
|
+
return new Promise((resolve, reject) => {
|
|
9254
|
+
import_https.default.get(url, (res) => {
|
|
9255
|
+
if (res.statusCode !== 200) {
|
|
9256
|
+
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9257
|
+
res.resume();
|
|
9258
|
+
return;
|
|
9259
|
+
}
|
|
9260
|
+
const chunks = [];
|
|
9261
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
9262
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9263
|
+
res.on("error", reject);
|
|
9264
|
+
}).on("error", reject);
|
|
9265
|
+
});
|
|
9266
|
+
}
|
|
9267
|
+
|
|
9268
|
+
// src/cli/commands/shield.ts
|
|
9269
|
+
var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
|
|
9399
9270
|
function registerShieldCommand(program2) {
|
|
9400
9271
|
const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
|
|
9401
9272
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
@@ -9455,7 +9326,32 @@ function registerShieldCommand(program2) {
|
|
|
9455
9326
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
9456
9327
|
`));
|
|
9457
9328
|
});
|
|
9458
|
-
shieldCmd.command("list").description("Show
|
|
9329
|
+
shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
|
|
9330
|
+
if (opts.community) {
|
|
9331
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
|
|
9332
|
+
console.log(import_chalk6.default.gray(" Fetching index\u2026\n"));
|
|
9333
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
|
|
9334
|
+
const entries = JSON.parse(body);
|
|
9335
|
+
const installed = new Set(listShields().map((s) => s.name));
|
|
9336
|
+
for (const e of entries) {
|
|
9337
|
+
const tag = installed.has(e.name) ? import_chalk6.default.green("installed") : import_chalk6.default.gray("available");
|
|
9338
|
+
console.log(
|
|
9339
|
+
` ${tag} ${import_chalk6.default.cyan(e.name.padEnd(12))} ${e.description} ${import_chalk6.default.gray(`by ${e.author}`)}`
|
|
9340
|
+
);
|
|
9341
|
+
}
|
|
9342
|
+
console.log("");
|
|
9343
|
+
console.log(
|
|
9344
|
+
import_chalk6.default.gray(` Install a shield: ${import_chalk6.default.cyan("node9 shield install <name>")}
|
|
9345
|
+
`)
|
|
9346
|
+
);
|
|
9347
|
+
}).catch((err2) => {
|
|
9348
|
+
console.error(import_chalk6.default.red(`
|
|
9349
|
+
\u274C Could not fetch community index: ${String(err2)}
|
|
9350
|
+
`));
|
|
9351
|
+
process.exit(1);
|
|
9352
|
+
});
|
|
9353
|
+
return;
|
|
9354
|
+
}
|
|
9459
9355
|
const active = new Set(readActiveShields());
|
|
9460
9356
|
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
9461
9357
|
for (const shield of listShields()) {
|
|
@@ -9465,6 +9361,10 @@ function registerShieldCommand(program2) {
|
|
|
9465
9361
|
console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
9466
9362
|
}
|
|
9467
9363
|
console.log("");
|
|
9364
|
+
console.log(
|
|
9365
|
+
import_chalk6.default.gray(` Browse community shields: ${import_chalk6.default.cyan("node9 shield list --community")}
|
|
9366
|
+
`)
|
|
9367
|
+
);
|
|
9468
9368
|
});
|
|
9469
9369
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
9470
9370
|
const active = readActiveShields();
|
|
@@ -9602,6 +9502,52 @@ function registerShieldCommand(program2) {
|
|
|
9602
9502
|
`)
|
|
9603
9503
|
);
|
|
9604
9504
|
});
|
|
9505
|
+
shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
|
|
9506
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
9507
|
+
console.error(
|
|
9508
|
+
import_chalk6.default.red(
|
|
9509
|
+
`
|
|
9510
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
9511
|
+
`
|
|
9512
|
+
)
|
|
9513
|
+
);
|
|
9514
|
+
process.exit(1);
|
|
9515
|
+
}
|
|
9516
|
+
console.log(import_chalk6.default.bold(`
|
|
9517
|
+
\u{1F6E1}\uFE0F Installing shield "${name}"\u2026
|
|
9518
|
+
`));
|
|
9519
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
|
|
9520
|
+
const entries = JSON.parse(indexBody);
|
|
9521
|
+
const entry = entries.find((e) => e.name === name);
|
|
9522
|
+
if (!entry) {
|
|
9523
|
+
const names = entries.map((e) => import_chalk6.default.cyan(e.name)).join(", ");
|
|
9524
|
+
console.error(
|
|
9525
|
+
import_chalk6.default.red(`\u274C Shield "${name}" not found in the community marketplace.
|
|
9526
|
+
`)
|
|
9527
|
+
);
|
|
9528
|
+
console.error(` Available: ${names}
|
|
9529
|
+
`);
|
|
9530
|
+
process.exit(1);
|
|
9531
|
+
}
|
|
9532
|
+
return httpsFetch(entry.url);
|
|
9533
|
+
}).then((shieldBody) => {
|
|
9534
|
+
const shieldJson = JSON.parse(shieldBody);
|
|
9535
|
+
installShield(name, shieldJson);
|
|
9536
|
+
console.log(
|
|
9537
|
+
import_chalk6.default.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
|
|
9538
|
+
);
|
|
9539
|
+
console.log(
|
|
9540
|
+
import_chalk6.default.gray(` Activate it with: ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
|
|
9541
|
+
`)
|
|
9542
|
+
);
|
|
9543
|
+
appendConfigAudit({ event: "shield-install", shield: name });
|
|
9544
|
+
}).catch((err2) => {
|
|
9545
|
+
console.error(import_chalk6.default.red(`
|
|
9546
|
+
\u274C Install failed: ${String(err2)}
|
|
9547
|
+
`));
|
|
9548
|
+
process.exit(1);
|
|
9549
|
+
});
|
|
9550
|
+
});
|
|
9605
9551
|
}
|
|
9606
9552
|
function registerConfigShowCommand(program2) {
|
|
9607
9553
|
program2.command("config show").description(
|
|
@@ -10135,7 +10081,7 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
10135
10081
|
var import_fs23 = __toESM(require("fs"));
|
|
10136
10082
|
var import_path25 = __toESM(require("path"));
|
|
10137
10083
|
var import_os19 = __toESM(require("os"));
|
|
10138
|
-
var
|
|
10084
|
+
var import_https2 = __toESM(require("https"));
|
|
10139
10085
|
init_core();
|
|
10140
10086
|
init_shields();
|
|
10141
10087
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
@@ -10147,7 +10093,7 @@ function fireTelemetryPing(agents) {
|
|
|
10147
10093
|
os: process.platform,
|
|
10148
10094
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10149
10095
|
});
|
|
10150
|
-
const req =
|
|
10096
|
+
const req = import_https2.default.request(
|
|
10151
10097
|
{
|
|
10152
10098
|
hostname: "api.node9.ai",
|
|
10153
10099
|
path: "/api/v1/telemetry",
|