@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.mjs
CHANGED
|
@@ -250,6 +250,70 @@ import fs2 from "fs";
|
|
|
250
250
|
import path2 from "path";
|
|
251
251
|
import os2 from "os";
|
|
252
252
|
import crypto from "crypto";
|
|
253
|
+
function validateShieldDefinition(raw, filePath) {
|
|
254
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
255
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
256
|
+
`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const r = raw;
|
|
260
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
261
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
262
|
+
`);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (typeof r.description !== "string") {
|
|
266
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
267
|
+
`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (!Array.isArray(r.aliases)) {
|
|
271
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
272
|
+
`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (!Array.isArray(r.smartRules)) {
|
|
276
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
277
|
+
`);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
281
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
282
|
+
`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return r;
|
|
286
|
+
}
|
|
287
|
+
function loadShieldsFromDir(dir, label) {
|
|
288
|
+
const result = {};
|
|
289
|
+
let entries;
|
|
290
|
+
try {
|
|
291
|
+
entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
292
|
+
} catch (err2) {
|
|
293
|
+
if (err2.code !== "ENOENT") {
|
|
294
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
for (const file of entries) {
|
|
300
|
+
const filePath = path2.join(dir, file);
|
|
301
|
+
try {
|
|
302
|
+
const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
303
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
304
|
+
if (shield) result[shield.name] = shield;
|
|
305
|
+
} catch (err2) {
|
|
306
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function buildSHIELDS() {
|
|
313
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
314
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
315
|
+
return { ...builtins, ...userShields };
|
|
316
|
+
}
|
|
253
317
|
function resolveShieldName(input) {
|
|
254
318
|
const lower = input.toLowerCase();
|
|
255
319
|
if (SHIELDS[lower]) return lower;
|
|
@@ -356,255 +420,30 @@ function resolveShieldRule(shieldName, identifier) {
|
|
|
356
420
|
}
|
|
357
421
|
return null;
|
|
358
422
|
}
|
|
359
|
-
|
|
423
|
+
function installShield(name, shieldJson) {
|
|
424
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
|
|
430
|
+
if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
|
|
431
|
+
if (shield.name !== name) {
|
|
432
|
+
throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
|
|
433
|
+
}
|
|
434
|
+
fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
|
|
435
|
+
const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
|
|
436
|
+
const tmp = `${filePath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
437
|
+
fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
|
|
438
|
+
fs2.renameSync(tmp, filePath);
|
|
439
|
+
}
|
|
440
|
+
var BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
|
|
360
441
|
var init_shields = __esm({
|
|
361
442
|
"src/shields.ts"() {
|
|
362
443
|
"use strict";
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
367
|
-
aliases: ["pg", "postgresql"],
|
|
368
|
-
smartRules: [
|
|
369
|
-
{
|
|
370
|
-
name: "shield:postgres:block-drop-table",
|
|
371
|
-
tool: "*",
|
|
372
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
373
|
-
verdict: "block",
|
|
374
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
name: "shield:postgres:block-truncate",
|
|
378
|
-
tool: "*",
|
|
379
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
380
|
-
verdict: "block",
|
|
381
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
name: "shield:postgres:block-drop-column",
|
|
385
|
-
tool: "*",
|
|
386
|
-
conditions: [
|
|
387
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
388
|
-
],
|
|
389
|
-
verdict: "block",
|
|
390
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
name: "shield:postgres:review-grant-revoke",
|
|
394
|
-
tool: "*",
|
|
395
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
396
|
-
verdict: "review",
|
|
397
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
398
|
-
}
|
|
399
|
-
],
|
|
400
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
401
|
-
},
|
|
402
|
-
github: {
|
|
403
|
-
name: "github",
|
|
404
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
405
|
-
aliases: ["git"],
|
|
406
|
-
smartRules: [
|
|
407
|
-
{
|
|
408
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
409
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
410
|
-
name: "shield:github:review-delete-branch-remote",
|
|
411
|
-
tool: "bash",
|
|
412
|
-
conditions: [
|
|
413
|
-
{
|
|
414
|
-
field: "command",
|
|
415
|
-
op: "matches",
|
|
416
|
-
value: "git\\s+push\\s+.*--delete",
|
|
417
|
-
flags: "i"
|
|
418
|
-
}
|
|
419
|
-
],
|
|
420
|
-
verdict: "review",
|
|
421
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
name: "shield:github:block-delete-repo",
|
|
425
|
-
tool: "*",
|
|
426
|
-
conditions: [
|
|
427
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
428
|
-
],
|
|
429
|
-
verdict: "block",
|
|
430
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
431
|
-
}
|
|
432
|
-
],
|
|
433
|
-
dangerousWords: []
|
|
434
|
-
},
|
|
435
|
-
aws: {
|
|
436
|
-
name: "aws",
|
|
437
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
438
|
-
aliases: ["amazon"],
|
|
439
|
-
smartRules: [
|
|
440
|
-
{
|
|
441
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
442
|
-
tool: "*",
|
|
443
|
-
conditions: [
|
|
444
|
-
{
|
|
445
|
-
field: "command",
|
|
446
|
-
op: "matches",
|
|
447
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
448
|
-
flags: "i"
|
|
449
|
-
}
|
|
450
|
-
],
|
|
451
|
-
verdict: "block",
|
|
452
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
name: "shield:aws:review-iam-changes",
|
|
456
|
-
tool: "*",
|
|
457
|
-
conditions: [
|
|
458
|
-
{
|
|
459
|
-
field: "command",
|
|
460
|
-
op: "matches",
|
|
461
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
462
|
-
flags: "i"
|
|
463
|
-
}
|
|
464
|
-
],
|
|
465
|
-
verdict: "review",
|
|
466
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: "shield:aws:block-ec2-terminate",
|
|
470
|
-
tool: "*",
|
|
471
|
-
conditions: [
|
|
472
|
-
{
|
|
473
|
-
field: "command",
|
|
474
|
-
op: "matches",
|
|
475
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
476
|
-
flags: "i"
|
|
477
|
-
}
|
|
478
|
-
],
|
|
479
|
-
verdict: "block",
|
|
480
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
name: "shield:aws:review-rds-delete",
|
|
484
|
-
tool: "*",
|
|
485
|
-
conditions: [
|
|
486
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
487
|
-
],
|
|
488
|
-
verdict: "review",
|
|
489
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
490
|
-
}
|
|
491
|
-
],
|
|
492
|
-
dangerousWords: []
|
|
493
|
-
},
|
|
494
|
-
"bash-safe": {
|
|
495
|
-
name: "bash-safe",
|
|
496
|
-
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
497
|
-
aliases: ["bash", "shell"],
|
|
498
|
-
smartRules: [
|
|
499
|
-
{
|
|
500
|
-
name: "shield:bash-safe:block-pipe-to-shell",
|
|
501
|
-
tool: "bash",
|
|
502
|
-
conditions: [
|
|
503
|
-
{
|
|
504
|
-
field: "command",
|
|
505
|
-
op: "matches",
|
|
506
|
-
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
507
|
-
flags: "i"
|
|
508
|
-
}
|
|
509
|
-
],
|
|
510
|
-
verdict: "block",
|
|
511
|
-
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
name: "shield:bash-safe:block-obfuscated-exec",
|
|
515
|
-
tool: "bash",
|
|
516
|
-
conditions: [
|
|
517
|
-
{
|
|
518
|
-
field: "command",
|
|
519
|
-
op: "matches",
|
|
520
|
-
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
521
|
-
flags: "i"
|
|
522
|
-
}
|
|
523
|
-
],
|
|
524
|
-
verdict: "block",
|
|
525
|
-
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
name: "shield:bash-safe:block-rm-root",
|
|
529
|
-
tool: "bash",
|
|
530
|
-
conditions: [
|
|
531
|
-
{
|
|
532
|
-
field: "command",
|
|
533
|
-
op: "matches",
|
|
534
|
-
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*$",
|
|
535
|
-
flags: "i"
|
|
536
|
-
}
|
|
537
|
-
],
|
|
538
|
-
verdict: "block",
|
|
539
|
-
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
540
|
-
},
|
|
541
|
-
{
|
|
542
|
-
name: "shield:bash-safe:block-disk-overwrite",
|
|
543
|
-
tool: "bash",
|
|
544
|
-
conditions: [
|
|
545
|
-
{
|
|
546
|
-
field: "command",
|
|
547
|
-
op: "matches",
|
|
548
|
-
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
549
|
-
flags: "i"
|
|
550
|
-
}
|
|
551
|
-
],
|
|
552
|
-
verdict: "block",
|
|
553
|
-
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
name: "shield:bash-safe:review-eval",
|
|
557
|
-
tool: "bash",
|
|
558
|
-
conditions: [
|
|
559
|
-
{
|
|
560
|
-
field: "command",
|
|
561
|
-
op: "matches",
|
|
562
|
-
value: '\\beval\\s+[\\$`("]',
|
|
563
|
-
flags: "i"
|
|
564
|
-
}
|
|
565
|
-
],
|
|
566
|
-
verdict: "review",
|
|
567
|
-
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
568
|
-
}
|
|
569
|
-
],
|
|
570
|
-
dangerousWords: []
|
|
571
|
-
},
|
|
572
|
-
filesystem: {
|
|
573
|
-
name: "filesystem",
|
|
574
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
575
|
-
aliases: ["fs"],
|
|
576
|
-
smartRules: [
|
|
577
|
-
{
|
|
578
|
-
name: "shield:filesystem:review-chmod-777",
|
|
579
|
-
tool: "bash",
|
|
580
|
-
conditions: [
|
|
581
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
582
|
-
],
|
|
583
|
-
verdict: "review",
|
|
584
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
name: "shield:filesystem:review-write-etc",
|
|
588
|
-
tool: "bash",
|
|
589
|
-
conditions: [
|
|
590
|
-
{
|
|
591
|
-
field: "command",
|
|
592
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
593
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
594
|
-
op: "matches",
|
|
595
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
596
|
-
}
|
|
597
|
-
],
|
|
598
|
-
verdict: "review",
|
|
599
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
600
|
-
}
|
|
601
|
-
],
|
|
602
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
603
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
604
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
605
|
-
dangerousWords: ["wipefs"]
|
|
606
|
-
}
|
|
607
|
-
};
|
|
444
|
+
BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
|
|
445
|
+
USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
446
|
+
SHIELDS = buildSHIELDS();
|
|
608
447
|
SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
|
|
609
448
|
}
|
|
610
449
|
});
|
|
@@ -2581,7 +2420,7 @@ function isDaemonRunning() {
|
|
|
2581
2420
|
return false;
|
|
2582
2421
|
}
|
|
2583
2422
|
}
|
|
2584
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2423
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2585
2424
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2586
2425
|
const ctrl = new AbortController();
|
|
2587
2426
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2602,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2602
2441
|
...cwd && { cwd },
|
|
2603
2442
|
...recoveryCommand && { recoveryCommand },
|
|
2604
2443
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2605
|
-
...viewOnly && { viewOnly: true }
|
|
2444
|
+
...viewOnly && { viewOnly: true },
|
|
2445
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2606
2446
|
}),
|
|
2607
2447
|
signal: ctrl.signal
|
|
2608
2448
|
});
|
|
@@ -3292,6 +3132,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3292
3132
|
let policyMatchedWord;
|
|
3293
3133
|
let riskMetadata;
|
|
3294
3134
|
let statefulRecoveryCommand;
|
|
3135
|
+
let localSmartRuleMatched = false;
|
|
3295
3136
|
let taintWarning = null;
|
|
3296
3137
|
if (isNetworkTool(toolName, args)) {
|
|
3297
3138
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3435,6 +3276,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3435
3276
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3436
3277
|
policyMatchedField = policyResult.matchedField;
|
|
3437
3278
|
policyMatchedWord = policyResult.matchedWord;
|
|
3279
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3438
3280
|
riskMetadata = computeRiskMetadata(
|
|
3439
3281
|
args,
|
|
3440
3282
|
policyResult.tier ?? 6,
|
|
@@ -3477,22 +3319,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3477
3319
|
}
|
|
3478
3320
|
let cloudRequestId = null;
|
|
3479
3321
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3480
|
-
if (cloudEnforced) {
|
|
3322
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3481
3323
|
try {
|
|
3482
3324
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3483
3325
|
if (!initResult.pending) {
|
|
3484
3326
|
if (initResult.shadowMode) {
|
|
3485
3327
|
return { approved: true, checkedBy: "cloud" };
|
|
3486
3328
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3329
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3330
|
+
return {
|
|
3331
|
+
approved: !!initResult.approved,
|
|
3332
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
3333
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
3334
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
3335
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3340
|
+
cloudRequestId = initResult.requestId || null;
|
|
3494
3341
|
}
|
|
3495
|
-
cloudRequestId = initResult.requestId || null;
|
|
3496
3342
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3497
3343
|
} catch {
|
|
3498
3344
|
}
|
|
@@ -3538,7 +3384,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3538
3384
|
riskMetadata,
|
|
3539
3385
|
options?.activityId,
|
|
3540
3386
|
options?.cwd,
|
|
3541
|
-
statefulRecoveryCommand
|
|
3387
|
+
statefulRecoveryCommand,
|
|
3388
|
+
void 0,
|
|
3389
|
+
void 0,
|
|
3390
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3542
3391
|
);
|
|
3543
3392
|
daemonEntryId = entry.id;
|
|
3544
3393
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3546,7 +3395,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3546
3395
|
}
|
|
3547
3396
|
}
|
|
3548
3397
|
}
|
|
3549
|
-
if (cloudEnforced && cloudRequestId) {
|
|
3398
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3550
3399
|
racePromises.push(
|
|
3551
3400
|
(async () => {
|
|
3552
3401
|
try {
|
|
@@ -6213,7 +6062,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6213
6062
|
viewOnly = false,
|
|
6214
6063
|
fromCLI = false,
|
|
6215
6064
|
activityId,
|
|
6216
|
-
cwd
|
|
6065
|
+
cwd,
|
|
6066
|
+
localSmartRuleMatched = false
|
|
6217
6067
|
} = JSON.parse(body);
|
|
6218
6068
|
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
6219
6069
|
const entry = {
|
|
@@ -6293,7 +6143,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6293
6143
|
agent: typeof agent === "string" ? agent : void 0,
|
|
6294
6144
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
6295
6145
|
},
|
|
6296
|
-
{ calledFromDaemon: true }
|
|
6146
|
+
{ calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
|
|
6297
6147
|
).then((result) => {
|
|
6298
6148
|
const e = pending.get(id);
|
|
6299
6149
|
if (!e) return;
|
|
@@ -9136,7 +8986,7 @@ RAW: ${raw}
|
|
|
9136
8986
|
}
|
|
9137
8987
|
}) + "\n"
|
|
9138
8988
|
);
|
|
9139
|
-
process.exit(
|
|
8989
|
+
process.exit(2);
|
|
9140
8990
|
};
|
|
9141
8991
|
if (!toolName) {
|
|
9142
8992
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
@@ -9371,6 +9221,27 @@ init_shields();
|
|
|
9371
9221
|
init_audit();
|
|
9372
9222
|
init_config();
|
|
9373
9223
|
import chalk6 from "chalk";
|
|
9224
|
+
|
|
9225
|
+
// src/utils/https-fetch.ts
|
|
9226
|
+
import https from "https";
|
|
9227
|
+
function httpsFetch(url) {
|
|
9228
|
+
return new Promise((resolve, reject) => {
|
|
9229
|
+
https.get(url, (res) => {
|
|
9230
|
+
if (res.statusCode !== 200) {
|
|
9231
|
+
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9232
|
+
res.resume();
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
const chunks = [];
|
|
9236
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
9237
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9238
|
+
res.on("error", reject);
|
|
9239
|
+
}).on("error", reject);
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9242
|
+
|
|
9243
|
+
// src/cli/commands/shield.ts
|
|
9244
|
+
var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
|
|
9374
9245
|
function registerShieldCommand(program2) {
|
|
9375
9246
|
const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
|
|
9376
9247
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
@@ -9430,7 +9301,32 @@ function registerShieldCommand(program2) {
|
|
|
9430
9301
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
9431
9302
|
`));
|
|
9432
9303
|
});
|
|
9433
|
-
shieldCmd.command("list").description("Show
|
|
9304
|
+
shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
|
|
9305
|
+
if (opts.community) {
|
|
9306
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
|
|
9307
|
+
console.log(chalk6.gray(" Fetching index\u2026\n"));
|
|
9308
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
|
|
9309
|
+
const entries = JSON.parse(body);
|
|
9310
|
+
const installed = new Set(listShields().map((s) => s.name));
|
|
9311
|
+
for (const e of entries) {
|
|
9312
|
+
const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
|
|
9313
|
+
console.log(
|
|
9314
|
+
` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
|
|
9315
|
+
);
|
|
9316
|
+
}
|
|
9317
|
+
console.log("");
|
|
9318
|
+
console.log(
|
|
9319
|
+
chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
|
|
9320
|
+
`)
|
|
9321
|
+
);
|
|
9322
|
+
}).catch((err2) => {
|
|
9323
|
+
console.error(chalk6.red(`
|
|
9324
|
+
\u274C Could not fetch community index: ${String(err2)}
|
|
9325
|
+
`));
|
|
9326
|
+
process.exit(1);
|
|
9327
|
+
});
|
|
9328
|
+
return;
|
|
9329
|
+
}
|
|
9434
9330
|
const active = new Set(readActiveShields());
|
|
9435
9331
|
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
9436
9332
|
for (const shield of listShields()) {
|
|
@@ -9440,6 +9336,10 @@ function registerShieldCommand(program2) {
|
|
|
9440
9336
|
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
9441
9337
|
}
|
|
9442
9338
|
console.log("");
|
|
9339
|
+
console.log(
|
|
9340
|
+
chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
|
|
9341
|
+
`)
|
|
9342
|
+
);
|
|
9443
9343
|
});
|
|
9444
9344
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
9445
9345
|
const active = readActiveShields();
|
|
@@ -9577,6 +9477,52 @@ function registerShieldCommand(program2) {
|
|
|
9577
9477
|
`)
|
|
9578
9478
|
);
|
|
9579
9479
|
});
|
|
9480
|
+
shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
|
|
9481
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
9482
|
+
console.error(
|
|
9483
|
+
chalk6.red(
|
|
9484
|
+
`
|
|
9485
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
9486
|
+
`
|
|
9487
|
+
)
|
|
9488
|
+
);
|
|
9489
|
+
process.exit(1);
|
|
9490
|
+
}
|
|
9491
|
+
console.log(chalk6.bold(`
|
|
9492
|
+
\u{1F6E1}\uFE0F Installing shield "${name}"\u2026
|
|
9493
|
+
`));
|
|
9494
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
|
|
9495
|
+
const entries = JSON.parse(indexBody);
|
|
9496
|
+
const entry = entries.find((e) => e.name === name);
|
|
9497
|
+
if (!entry) {
|
|
9498
|
+
const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
|
|
9499
|
+
console.error(
|
|
9500
|
+
chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
|
|
9501
|
+
`)
|
|
9502
|
+
);
|
|
9503
|
+
console.error(` Available: ${names}
|
|
9504
|
+
`);
|
|
9505
|
+
process.exit(1);
|
|
9506
|
+
}
|
|
9507
|
+
return httpsFetch(entry.url);
|
|
9508
|
+
}).then((shieldBody) => {
|
|
9509
|
+
const shieldJson = JSON.parse(shieldBody);
|
|
9510
|
+
installShield(name, shieldJson);
|
|
9511
|
+
console.log(
|
|
9512
|
+
chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
|
|
9513
|
+
);
|
|
9514
|
+
console.log(
|
|
9515
|
+
chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
|
|
9516
|
+
`)
|
|
9517
|
+
);
|
|
9518
|
+
appendConfigAudit({ event: "shield-install", shield: name });
|
|
9519
|
+
}).catch((err2) => {
|
|
9520
|
+
console.error(chalk6.red(`
|
|
9521
|
+
\u274C Install failed: ${String(err2)}
|
|
9522
|
+
`));
|
|
9523
|
+
process.exit(1);
|
|
9524
|
+
});
|
|
9525
|
+
});
|
|
9580
9526
|
}
|
|
9581
9527
|
function registerConfigShowCommand(program2) {
|
|
9582
9528
|
program2.command("config show").description(
|
|
@@ -10111,7 +10057,7 @@ import chalk11 from "chalk";
|
|
|
10111
10057
|
import fs23 from "fs";
|
|
10112
10058
|
import path25 from "path";
|
|
10113
10059
|
import os19 from "os";
|
|
10114
|
-
import
|
|
10060
|
+
import https2 from "https";
|
|
10115
10061
|
init_shields();
|
|
10116
10062
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10117
10063
|
function fireTelemetryPing(agents) {
|
|
@@ -10122,7 +10068,7 @@ function fireTelemetryPing(agents) {
|
|
|
10122
10068
|
os: process.platform,
|
|
10123
10069
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10124
10070
|
});
|
|
10125
|
-
const req =
|
|
10071
|
+
const req = https2.request(
|
|
10126
10072
|
{
|
|
10127
10073
|
hostname: "api.node9.ai",
|
|
10128
10074
|
path: "/api/v1/telemetry",
|