@node9/proxy 2.37.1 → 2.39.0
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/dist/cli.js +7112 -3805
- package/dist/cli.mjs +7002 -3694
- package/dist/dashboard.mjs +1351 -369
- package/dist/index.js +2333 -710
- package/dist/index.mjs +2326 -703
- package/dist/scan-ink.mjs +1340 -279
- package/package.json +1 -1
package/dist/scan-ink.mjs
CHANGED
|
@@ -67,7 +67,8 @@ var SmartConditionSchema = z.object({
|
|
|
67
67
|
}
|
|
68
68
|
),
|
|
69
69
|
value: z.string().optional(),
|
|
70
|
-
flags: z.string().optional()
|
|
70
|
+
flags: z.string().optional(),
|
|
71
|
+
reading: z.enum(["access"]).optional()
|
|
71
72
|
}).refine(
|
|
72
73
|
(c) => {
|
|
73
74
|
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
@@ -78,6 +79,9 @@ var SmartConditionSchema = z.object({
|
|
|
78
79
|
var SmartRuleSchema = z.object({
|
|
79
80
|
name: z.string().optional(),
|
|
80
81
|
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
82
|
+
// Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
|
|
83
|
+
server: z.string().min(1).optional(),
|
|
84
|
+
notServer: z.string().min(1).optional(),
|
|
81
85
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
82
86
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
83
87
|
verdict: z.enum(["allow", "review", "block"], {
|
|
@@ -98,7 +102,8 @@ var SmartRuleSchema = z.object({
|
|
|
98
102
|
var ConfigFileSchema = z.object({
|
|
99
103
|
version: z.string().optional(),
|
|
100
104
|
settings: z.object({
|
|
101
|
-
|
|
105
|
+
// The protection level (levels.ts in the engine); `observe` is Audit's older name.
|
|
106
|
+
mode: z.enum(["observe", "audit", "soft", "standard", "strict", "block-all"]).optional(),
|
|
102
107
|
autoStartDaemon: z.boolean().optional(),
|
|
103
108
|
// enableUndo / policy.snapshot: removed with the undo feature. The
|
|
104
109
|
// schema strips undeclared keys silently (plain zod object — no
|
|
@@ -236,12 +241,24 @@ import picomatch from "picomatch";
|
|
|
236
241
|
import safeRegex from "safe-regex2";
|
|
237
242
|
import pm from "picomatch";
|
|
238
243
|
import mvdanSh from "mvdan-sh";
|
|
239
|
-
import safeRegex2 from "safe-regex2";
|
|
240
244
|
import mvdanSh2 from "mvdan-sh";
|
|
241
|
-
import
|
|
245
|
+
import safeRegex2 from "safe-regex2";
|
|
242
246
|
import { createHash as createHash2 } from "crypto";
|
|
243
247
|
import mvdanSh3 from "mvdan-sh";
|
|
244
248
|
import mvdanSh4 from "mvdan-sh";
|
|
249
|
+
var MAX_REGEX_LENGTH = 256;
|
|
250
|
+
function validateRegex(pattern) {
|
|
251
|
+
if (!pattern) return "Pattern is required";
|
|
252
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
253
|
+
try {
|
|
254
|
+
new RegExp(pattern);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
257
|
+
}
|
|
258
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
259
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
245
262
|
var aws_default = {
|
|
246
263
|
name: "aws",
|
|
247
264
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -543,77 +560,6 @@ var filesystem_default = {
|
|
|
543
560
|
],
|
|
544
561
|
dangerousWords: ["wipefs"]
|
|
545
562
|
};
|
|
546
|
-
var finance_default = {
|
|
547
|
-
name: "finance",
|
|
548
|
-
description: "Asks before money moves through a payment or banking app (refunds, payouts, transfers, charges, invoices), whatever the request said. Reads are never asked about.",
|
|
549
|
-
aliases: ["payments", "money"],
|
|
550
|
-
smartRules: [
|
|
551
|
-
{
|
|
552
|
-
name: "shield:finance:review-refund",
|
|
553
|
-
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
554
|
-
conditions: [],
|
|
555
|
-
verdict: "review",
|
|
556
|
-
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
name: "shield:finance:review-payout",
|
|
560
|
-
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
561
|
-
conditions: [],
|
|
562
|
-
verdict: "review",
|
|
563
|
-
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: "shield:finance:review-transfer",
|
|
567
|
-
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction,create_transaction}",
|
|
568
|
-
conditions: [],
|
|
569
|
-
verdict: "review",
|
|
570
|
-
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
name: "shield:finance:review-charge",
|
|
574
|
-
tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
|
|
575
|
-
conditions: [],
|
|
576
|
-
verdict: "review",
|
|
577
|
-
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
name: "shield:finance:review-payment-link",
|
|
581
|
-
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
582
|
-
conditions: [],
|
|
583
|
-
verdict: "review",
|
|
584
|
-
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
name: "shield:finance:review-subscription",
|
|
588
|
-
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
589
|
-
conditions: [],
|
|
590
|
-
verdict: "review",
|
|
591
|
-
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
name: "shield:finance:review-dispute",
|
|
595
|
-
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
596
|
-
conditions: [],
|
|
597
|
-
verdict: "review",
|
|
598
|
-
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
name: "shield:finance:review-card",
|
|
602
|
-
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
603
|
-
conditions: [],
|
|
604
|
-
verdict: "review",
|
|
605
|
-
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
name: "shield:finance:review-withdraw",
|
|
609
|
-
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
610
|
-
conditions: [],
|
|
611
|
-
verdict: "review",
|
|
612
|
-
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
613
|
-
}
|
|
614
|
-
],
|
|
615
|
-
dangerousWords: []
|
|
616
|
-
};
|
|
617
563
|
var github_default = {
|
|
618
564
|
name: "github",
|
|
619
565
|
description: "Protects GitHub repositories from destructive AI operations",
|
|
@@ -640,56 +586,6 @@ var github_default = {
|
|
|
640
586
|
],
|
|
641
587
|
dangerousWords: []
|
|
642
588
|
};
|
|
643
|
-
var identity_default = {
|
|
644
|
-
name: "identity",
|
|
645
|
-
description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
|
|
646
|
-
aliases: ["access", "iam"],
|
|
647
|
-
smartRules: [
|
|
648
|
-
{
|
|
649
|
-
name: "shield:identity:review-password",
|
|
650
|
-
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
651
|
-
conditions: [],
|
|
652
|
-
verdict: "review",
|
|
653
|
-
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
name: "shield:identity:review-key",
|
|
657
|
-
tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
|
|
658
|
-
conditions: [],
|
|
659
|
-
verdict: "review",
|
|
660
|
-
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
name: "shield:identity:review-access",
|
|
664
|
-
tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy}",
|
|
665
|
-
conditions: [],
|
|
666
|
-
verdict: "review",
|
|
667
|
-
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
name: "shield:identity:review-membership",
|
|
671
|
-
tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member}",
|
|
672
|
-
conditions: [],
|
|
673
|
-
verdict: "review",
|
|
674
|
-
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
675
|
-
},
|
|
676
|
-
{
|
|
677
|
-
name: "shield:identity:review-user",
|
|
678
|
-
tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
679
|
-
conditions: [],
|
|
680
|
-
verdict: "review",
|
|
681
|
-
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
name: "shield:identity:review-mfa",
|
|
685
|
-
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
686
|
-
conditions: [],
|
|
687
|
-
verdict: "review",
|
|
688
|
-
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
689
|
-
}
|
|
690
|
-
],
|
|
691
|
-
dangerousWords: []
|
|
692
|
-
};
|
|
693
589
|
var k8s_default = {
|
|
694
590
|
name: "k8s",
|
|
695
591
|
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
@@ -942,7 +838,7 @@ var project_jail_default = {
|
|
|
942
838
|
{
|
|
943
839
|
field: "command",
|
|
944
840
|
op: "matches",
|
|
945
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
841
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development)){0,2}(?=[\\s;&|>)<]|$)",
|
|
946
842
|
flags: "i"
|
|
947
843
|
}
|
|
948
844
|
],
|
|
@@ -956,7 +852,7 @@ var project_jail_default = {
|
|
|
956
852
|
{
|
|
957
853
|
field: "command",
|
|
958
854
|
op: "matches",
|
|
959
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
855
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
960
856
|
flags: "i"
|
|
961
857
|
}
|
|
962
858
|
],
|
|
@@ -1022,6 +918,206 @@ var project_jail_default = {
|
|
|
1022
918
|
],
|
|
1023
919
|
dangerousWords: []
|
|
1024
920
|
};
|
|
921
|
+
var redis_default = {
|
|
922
|
+
name: "redis",
|
|
923
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
924
|
+
aliases: [],
|
|
925
|
+
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
926
|
+
smartRules: [
|
|
927
|
+
{
|
|
928
|
+
name: "shield:redis:block-flushall",
|
|
929
|
+
tool: "*",
|
|
930
|
+
conditions: [
|
|
931
|
+
{
|
|
932
|
+
field: "command",
|
|
933
|
+
op: "matches",
|
|
934
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
935
|
+
flags: "i"
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
verdict: "block",
|
|
939
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
name: "shield:redis:block-flushdb",
|
|
943
|
+
tool: "*",
|
|
944
|
+
conditions: [
|
|
945
|
+
{
|
|
946
|
+
field: "command",
|
|
947
|
+
op: "matches",
|
|
948
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
949
|
+
flags: "i"
|
|
950
|
+
}
|
|
951
|
+
],
|
|
952
|
+
verdict: "block",
|
|
953
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "shield:redis:block-config-resetstat",
|
|
957
|
+
tool: "*",
|
|
958
|
+
conditions: [
|
|
959
|
+
{
|
|
960
|
+
field: "command",
|
|
961
|
+
op: "matches",
|
|
962
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
963
|
+
flags: "i"
|
|
964
|
+
}
|
|
965
|
+
],
|
|
966
|
+
verdict: "block",
|
|
967
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: "shield:redis:review-config-set",
|
|
971
|
+
tool: "*",
|
|
972
|
+
conditions: [
|
|
973
|
+
{
|
|
974
|
+
field: "command",
|
|
975
|
+
op: "matches",
|
|
976
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
977
|
+
flags: "i"
|
|
978
|
+
}
|
|
979
|
+
],
|
|
980
|
+
verdict: "review",
|
|
981
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
name: "shield:redis:review-del-wildcard",
|
|
985
|
+
tool: "*",
|
|
986
|
+
conditions: [
|
|
987
|
+
{
|
|
988
|
+
field: "command",
|
|
989
|
+
op: "matches",
|
|
990
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
991
|
+
flags: "i"
|
|
992
|
+
}
|
|
993
|
+
],
|
|
994
|
+
verdict: "review",
|
|
995
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
996
|
+
}
|
|
997
|
+
],
|
|
998
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
999
|
+
};
|
|
1000
|
+
var finance_default = {
|
|
1001
|
+
name: "finance",
|
|
1002
|
+
description: "Asks before money moves through a payment or banking app (refunds, payouts, transfers, charges, invoices), whatever the request said. Reads are never asked about.",
|
|
1003
|
+
aliases: ["payments", "money"],
|
|
1004
|
+
smartRules: [
|
|
1005
|
+
{
|
|
1006
|
+
name: "shield:finance:review-refund",
|
|
1007
|
+
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
1008
|
+
conditions: [],
|
|
1009
|
+
verdict: "review",
|
|
1010
|
+
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "shield:finance:review-payout",
|
|
1014
|
+
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
1015
|
+
conditions: [],
|
|
1016
|
+
verdict: "review",
|
|
1017
|
+
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "shield:finance:review-transfer",
|
|
1021
|
+
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
|
|
1022
|
+
conditions: [],
|
|
1023
|
+
verdict: "review",
|
|
1024
|
+
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "shield:finance:review-charge",
|
|
1028
|
+
tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
|
|
1029
|
+
conditions: [],
|
|
1030
|
+
verdict: "review",
|
|
1031
|
+
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "shield:finance:review-payment-link",
|
|
1035
|
+
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
1036
|
+
conditions: [],
|
|
1037
|
+
verdict: "review",
|
|
1038
|
+
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
name: "shield:finance:review-subscription",
|
|
1042
|
+
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
1043
|
+
conditions: [],
|
|
1044
|
+
verdict: "review",
|
|
1045
|
+
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
name: "shield:finance:review-dispute",
|
|
1049
|
+
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
1050
|
+
conditions: [],
|
|
1051
|
+
verdict: "review",
|
|
1052
|
+
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
name: "shield:finance:review-card",
|
|
1056
|
+
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
1057
|
+
conditions: [],
|
|
1058
|
+
verdict: "review",
|
|
1059
|
+
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "shield:finance:review-withdraw",
|
|
1063
|
+
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
1064
|
+
conditions: [],
|
|
1065
|
+
verdict: "review",
|
|
1066
|
+
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
1067
|
+
}
|
|
1068
|
+
],
|
|
1069
|
+
dangerousWords: []
|
|
1070
|
+
};
|
|
1071
|
+
var identity_default = {
|
|
1072
|
+
name: "identity",
|
|
1073
|
+
description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
|
|
1074
|
+
aliases: ["access", "iam"],
|
|
1075
|
+
smartRules: [
|
|
1076
|
+
{
|
|
1077
|
+
name: "shield:identity:review-password",
|
|
1078
|
+
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
1079
|
+
conditions: [],
|
|
1080
|
+
verdict: "review",
|
|
1081
|
+
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
name: "shield:identity:review-key",
|
|
1085
|
+
tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
|
|
1086
|
+
conditions: [],
|
|
1087
|
+
verdict: "review",
|
|
1088
|
+
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
name: "shield:identity:review-access",
|
|
1092
|
+
tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy,share_file}",
|
|
1093
|
+
conditions: [],
|
|
1094
|
+
verdict: "review",
|
|
1095
|
+
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: "shield:identity:review-membership",
|
|
1099
|
+
tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member,add_user_to_channel,remove_user_from_slack}",
|
|
1100
|
+
conditions: [],
|
|
1101
|
+
verdict: "review",
|
|
1102
|
+
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
name: "shield:identity:review-user",
|
|
1106
|
+
tool: "{,mcp__*__}{create_user,delete_user,delete_deactivated_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
1107
|
+
conditions: [],
|
|
1108
|
+
verdict: "review",
|
|
1109
|
+
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
name: "shield:identity:review-mfa",
|
|
1113
|
+
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
1114
|
+
conditions: [],
|
|
1115
|
+
verdict: "review",
|
|
1116
|
+
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
1117
|
+
}
|
|
1118
|
+
],
|
|
1119
|
+
dangerousWords: []
|
|
1120
|
+
};
|
|
1025
1121
|
var publishing_default = {
|
|
1026
1122
|
name: "publishing",
|
|
1027
1123
|
description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
|
|
@@ -1106,170 +1202,634 @@ var publishing_default = {
|
|
|
1106
1202
|
},
|
|
1107
1203
|
{
|
|
1108
1204
|
name: "shield:publishing:review-python-publish",
|
|
1109
|
-
tool: "bash",
|
|
1110
|
-
conditions: [
|
|
1111
|
-
{
|
|
1112
|
-
field: "command",
|
|
1113
|
-
op: "matches",
|
|
1114
|
-
value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1115
|
-
flags: "i"
|
|
1116
|
-
}
|
|
1117
|
-
],
|
|
1118
|
-
verdict: "review",
|
|
1119
|
-
reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
|
|
1120
|
-
},
|
|
1121
|
-
{
|
|
1122
|
-
name: "shield:publishing:review-release-tool-publish",
|
|
1123
|
-
tool: "bash",
|
|
1124
|
-
conditions: [
|
|
1125
|
-
{
|
|
1126
|
-
field: "command",
|
|
1127
|
-
op: "matches",
|
|
1128
|
-
value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1129
|
-
flags: "i"
|
|
1130
|
-
}
|
|
1131
|
-
],
|
|
1132
|
-
verdict: "review",
|
|
1133
|
-
reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
|
|
1134
|
-
},
|
|
1135
|
-
{
|
|
1136
|
-
name: "shield:publishing:review-cargo-publish",
|
|
1137
|
-
tool: "bash",
|
|
1138
|
-
conditions: [
|
|
1139
|
-
{
|
|
1140
|
-
field: "command",
|
|
1141
|
-
op: "matches",
|
|
1142
|
-
value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1143
|
-
flags: "i"
|
|
1144
|
-
}
|
|
1145
|
-
],
|
|
1146
|
-
verdict: "review",
|
|
1147
|
-
reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
|
|
1148
|
-
},
|
|
1149
|
-
{
|
|
1150
|
-
name: "shield:publishing:review-gem-push",
|
|
1151
|
-
tool: "bash",
|
|
1152
|
-
conditions: [
|
|
1153
|
-
{
|
|
1154
|
-
field: "command",
|
|
1155
|
-
op: "matches",
|
|
1156
|
-
value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
|
|
1157
|
-
flags: "i"
|
|
1158
|
-
}
|
|
1159
|
-
],
|
|
1160
|
-
verdict: "review",
|
|
1161
|
-
reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
|
|
1162
|
-
},
|
|
1163
|
-
{
|
|
1164
|
-
name: "shield:publishing:review-docker-push",
|
|
1165
|
-
tool: "bash",
|
|
1166
|
-
conditions: [
|
|
1167
|
-
{
|
|
1168
|
-
field: "command",
|
|
1169
|
-
op: "matches",
|
|
1170
|
-
value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
|
|
1171
|
-
flags: "i"
|
|
1172
|
-
}
|
|
1173
|
-
],
|
|
1174
|
-
verdict: "review",
|
|
1175
|
-
reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
|
|
1176
|
-
},
|
|
1177
|
-
{
|
|
1178
|
-
name: "shield:publishing:review-gh-release",
|
|
1179
|
-
tool: "bash",
|
|
1180
|
-
conditions: [
|
|
1181
|
-
{
|
|
1182
|
-
field: "command",
|
|
1183
|
-
op: "matches",
|
|
1184
|
-
value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
|
|
1185
|
-
flags: "i"
|
|
1186
|
-
}
|
|
1187
|
-
],
|
|
1188
|
-
verdict: "review",
|
|
1189
|
-
reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
|
|
1190
|
-
}
|
|
1191
|
-
],
|
|
1192
|
-
dangerousWords: []
|
|
1193
|
-
};
|
|
1194
|
-
var redis_default = {
|
|
1195
|
-
name: "redis",
|
|
1196
|
-
description: "Protects Redis instances from destructive AI operations",
|
|
1197
|
-
aliases: [],
|
|
1198
|
-
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
1199
|
-
smartRules: [
|
|
1205
|
+
tool: "bash",
|
|
1206
|
+
conditions: [
|
|
1207
|
+
{
|
|
1208
|
+
field: "command",
|
|
1209
|
+
op: "matches",
|
|
1210
|
+
value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1211
|
+
flags: "i"
|
|
1212
|
+
}
|
|
1213
|
+
],
|
|
1214
|
+
verdict: "review",
|
|
1215
|
+
reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
|
|
1216
|
+
},
|
|
1200
1217
|
{
|
|
1201
|
-
name: "shield:
|
|
1202
|
-
tool: "
|
|
1218
|
+
name: "shield:publishing:review-release-tool-publish",
|
|
1219
|
+
tool: "bash",
|
|
1203
1220
|
conditions: [
|
|
1204
1221
|
{
|
|
1205
1222
|
field: "command",
|
|
1206
1223
|
op: "matches",
|
|
1207
|
-
value: "(
|
|
1224
|
+
value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1208
1225
|
flags: "i"
|
|
1209
1226
|
}
|
|
1210
1227
|
],
|
|
1211
|
-
verdict: "
|
|
1212
|
-
reason: "
|
|
1228
|
+
verdict: "review",
|
|
1229
|
+
reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
|
|
1213
1230
|
},
|
|
1214
1231
|
{
|
|
1215
|
-
name: "shield:
|
|
1216
|
-
tool: "
|
|
1232
|
+
name: "shield:publishing:review-cargo-publish",
|
|
1233
|
+
tool: "bash",
|
|
1217
1234
|
conditions: [
|
|
1218
1235
|
{
|
|
1219
1236
|
field: "command",
|
|
1220
1237
|
op: "matches",
|
|
1221
|
-
value: "(
|
|
1238
|
+
value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
1222
1239
|
flags: "i"
|
|
1223
1240
|
}
|
|
1224
1241
|
],
|
|
1225
|
-
verdict: "
|
|
1226
|
-
reason: "
|
|
1242
|
+
verdict: "review",
|
|
1243
|
+
reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
|
|
1227
1244
|
},
|
|
1228
1245
|
{
|
|
1229
|
-
name: "shield:
|
|
1230
|
-
tool: "
|
|
1246
|
+
name: "shield:publishing:review-gem-push",
|
|
1247
|
+
tool: "bash",
|
|
1231
1248
|
conditions: [
|
|
1232
1249
|
{
|
|
1233
1250
|
field: "command",
|
|
1234
1251
|
op: "matches",
|
|
1235
|
-
value: "(
|
|
1252
|
+
value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
|
|
1236
1253
|
flags: "i"
|
|
1237
1254
|
}
|
|
1238
1255
|
],
|
|
1239
|
-
verdict: "
|
|
1240
|
-
reason: "
|
|
1256
|
+
verdict: "review",
|
|
1257
|
+
reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
|
|
1241
1258
|
},
|
|
1242
1259
|
{
|
|
1243
|
-
name: "shield:
|
|
1244
|
-
tool: "
|
|
1260
|
+
name: "shield:publishing:review-docker-push",
|
|
1261
|
+
tool: "bash",
|
|
1245
1262
|
conditions: [
|
|
1246
1263
|
{
|
|
1247
1264
|
field: "command",
|
|
1248
1265
|
op: "matches",
|
|
1249
|
-
value: "(
|
|
1266
|
+
value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
|
|
1250
1267
|
flags: "i"
|
|
1251
1268
|
}
|
|
1252
1269
|
],
|
|
1253
1270
|
verdict: "review",
|
|
1254
|
-
reason: "
|
|
1271
|
+
reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
|
|
1255
1272
|
},
|
|
1256
1273
|
{
|
|
1257
|
-
name: "shield:
|
|
1258
|
-
tool: "
|
|
1274
|
+
name: "shield:publishing:review-gh-release",
|
|
1275
|
+
tool: "bash",
|
|
1259
1276
|
conditions: [
|
|
1260
1277
|
{
|
|
1261
1278
|
field: "command",
|
|
1262
1279
|
op: "matches",
|
|
1263
|
-
value: "(
|
|
1280
|
+
value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
|
|
1264
1281
|
flags: "i"
|
|
1265
1282
|
}
|
|
1266
1283
|
],
|
|
1267
1284
|
verdict: "review",
|
|
1268
|
-
reason: "
|
|
1285
|
+
reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
|
|
1269
1286
|
}
|
|
1270
1287
|
],
|
|
1271
|
-
dangerousWords: [
|
|
1288
|
+
dangerousWords: []
|
|
1289
|
+
};
|
|
1290
|
+
var ACTION_TEMPLATES = {
|
|
1291
|
+
finance: finance_default.smartRules,
|
|
1292
|
+
identity: identity_default.smartRules,
|
|
1293
|
+
publishing: publishing_default.smartRules
|
|
1294
|
+
};
|
|
1295
|
+
var ACTION_CHECKS = {
|
|
1296
|
+
finance: { checkId: "actions.money", slug: "money", label: "Money check" },
|
|
1297
|
+
identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
|
|
1298
|
+
publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
|
|
1299
|
+
};
|
|
1300
|
+
function isToolRule(rule) {
|
|
1301
|
+
return rule.tool.startsWith("{,mcp__*__}");
|
|
1302
|
+
}
|
|
1303
|
+
function actionPart(ruleName) {
|
|
1304
|
+
const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
|
|
1305
|
+
return m ? m[1] : ruleName ?? "rule";
|
|
1306
|
+
}
|
|
1307
|
+
function ownReason(reason, owner) {
|
|
1308
|
+
if (!reason) return reason;
|
|
1309
|
+
return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
|
|
1310
|
+
}
|
|
1311
|
+
var GAP = "[^;&|]{0,200}?";
|
|
1312
|
+
var prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
|
|
1313
|
+
var httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
|
|
1314
|
+
var httpWrite = (methods) => ({
|
|
1315
|
+
field: "command",
|
|
1316
|
+
op: "matches",
|
|
1317
|
+
value: `-X\\s*(?:${methods})|--(?:request|method)[=\\s]+(?:${methods})|--post-(?:data|file)|\\s(?:${methods}|${methods.toLowerCase()})\\s|^(?!.*\\s(?:-[a-zA-Z]*G|--get)(?![\\w-])).*(?:\\s-d[\\s'"@]|\\s--data|\\s--json\\s|\\s-F\\s|\\s--form\\s)`
|
|
1318
|
+
});
|
|
1319
|
+
var HTTP_WRITE = httpWrite("POST|DELETE");
|
|
1320
|
+
var HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
|
|
1321
|
+
var NOT_STRIPE_TEST = {
|
|
1322
|
+
field: "command",
|
|
1323
|
+
op: "notMatches",
|
|
1324
|
+
value: "\\b[sr]k_test_"
|
|
1272
1325
|
};
|
|
1326
|
+
var postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
|
|
1327
|
+
var STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
|
|
1328
|
+
var APP_SHIELD_SPECS = [
|
|
1329
|
+
{
|
|
1330
|
+
name: "stripe",
|
|
1331
|
+
label: "Stripe",
|
|
1332
|
+
description: "Asks before money moves in Stripe (refunds, payouts, charges, invoices, subscriptions), whether the agent uses the Stripe MCP server, the stripe CLI, the API or the Stripe library in a script. Reads are never asked about.",
|
|
1333
|
+
template: "finance",
|
|
1334
|
+
server: "*stripe*",
|
|
1335
|
+
doors: [
|
|
1336
|
+
{
|
|
1337
|
+
name: "shield:stripe:review-cli",
|
|
1338
|
+
tool: "bash",
|
|
1339
|
+
conditions: [
|
|
1340
|
+
{
|
|
1341
|
+
field: "command",
|
|
1342
|
+
op: "matches",
|
|
1343
|
+
value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
|
|
1344
|
+
flags: "i"
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
field: "command",
|
|
1348
|
+
op: "matches",
|
|
1349
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
1350
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
1351
|
+
}
|
|
1352
|
+
],
|
|
1353
|
+
conditionMode: "all",
|
|
1354
|
+
verdict: "review",
|
|
1355
|
+
reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
name: "shield:stripe:review-cli-request",
|
|
1359
|
+
tool: "bash",
|
|
1360
|
+
conditions: [
|
|
1361
|
+
{
|
|
1362
|
+
field: "command",
|
|
1363
|
+
op: "matches",
|
|
1364
|
+
value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
|
|
1365
|
+
flags: "i"
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
field: "command",
|
|
1369
|
+
op: "matches",
|
|
1370
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
1371
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
1372
|
+
}
|
|
1373
|
+
],
|
|
1374
|
+
conditionMode: "all",
|
|
1375
|
+
verdict: "review",
|
|
1376
|
+
reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
name: "shield:stripe:review-api",
|
|
1380
|
+
tool: "bash",
|
|
1381
|
+
conditions: [
|
|
1382
|
+
{
|
|
1383
|
+
field: "command",
|
|
1384
|
+
op: "matches",
|
|
1385
|
+
value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
|
|
1386
|
+
flags: "i"
|
|
1387
|
+
},
|
|
1388
|
+
HTTP_WRITE,
|
|
1389
|
+
NOT_STRIPE_TEST
|
|
1390
|
+
],
|
|
1391
|
+
conditionMode: "all",
|
|
1392
|
+
verdict: "review",
|
|
1393
|
+
reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
name: "shield:stripe:review-code",
|
|
1397
|
+
tool: "bash",
|
|
1398
|
+
conditions: [
|
|
1399
|
+
{
|
|
1400
|
+
field: "command",
|
|
1401
|
+
op: "matches",
|
|
1402
|
+
// Python: stripe.Refund.create(...)
|
|
1403
|
+
value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
|
|
1404
|
+
},
|
|
1405
|
+
NOT_STRIPE_TEST
|
|
1406
|
+
],
|
|
1407
|
+
conditionMode: "all",
|
|
1408
|
+
verdict: "review",
|
|
1409
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
name: "shield:stripe:review-code-node",
|
|
1413
|
+
tool: "bash",
|
|
1414
|
+
conditions: [
|
|
1415
|
+
{
|
|
1416
|
+
field: "command",
|
|
1417
|
+
op: "matches",
|
|
1418
|
+
// Node: stripe.refunds.create(...)
|
|
1419
|
+
value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
|
|
1420
|
+
},
|
|
1421
|
+
NOT_STRIPE_TEST
|
|
1422
|
+
],
|
|
1423
|
+
conditionMode: "all",
|
|
1424
|
+
verdict: "review",
|
|
1425
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
name: "shield:stripe:review-code-http",
|
|
1429
|
+
tool: "bash",
|
|
1430
|
+
conditions: [
|
|
1431
|
+
{
|
|
1432
|
+
field: "command",
|
|
1433
|
+
op: "matches",
|
|
1434
|
+
// requests.post("https://api.stripe.com/v1/refunds", ...)
|
|
1435
|
+
value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
|
|
1436
|
+
},
|
|
1437
|
+
NOT_STRIPE_TEST
|
|
1438
|
+
],
|
|
1439
|
+
conditionMode: "all",
|
|
1440
|
+
verdict: "review",
|
|
1441
|
+
reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
|
|
1442
|
+
}
|
|
1443
|
+
]
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
name: "paypal",
|
|
1447
|
+
label: "PayPal",
|
|
1448
|
+
description: "Asks before money moves in PayPal (refunds, payouts, captured orders, invoices, subscriptions), whether the agent uses a PayPal MCP server, the API or the PayPal SDK in a script. The sandbox is never asked about, and neither are reads.",
|
|
1449
|
+
template: "finance",
|
|
1450
|
+
server: "*paypal*",
|
|
1451
|
+
doors: [
|
|
1452
|
+
{
|
|
1453
|
+
name: "shield:paypal:review-api",
|
|
1454
|
+
tool: "bash",
|
|
1455
|
+
conditions: [
|
|
1456
|
+
{
|
|
1457
|
+
field: "command",
|
|
1458
|
+
op: "matches",
|
|
1459
|
+
// The live hosts only: api.paypal.com and api-m.paypal.com. The
|
|
1460
|
+
// sandbox (api-m.sandbox.paypal.com) moves no money.
|
|
1461
|
+
value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
|
|
1462
|
+
flags: "i"
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
field: "command",
|
|
1466
|
+
op: "matches",
|
|
1467
|
+
// The endpoints that move money.
|
|
1468
|
+
value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
|
|
1469
|
+
flags: "i"
|
|
1470
|
+
},
|
|
1471
|
+
HTTP_WRITE
|
|
1472
|
+
],
|
|
1473
|
+
conditionMode: "all",
|
|
1474
|
+
verdict: "review",
|
|
1475
|
+
reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
name: "shield:paypal:review-code",
|
|
1479
|
+
tool: "bash",
|
|
1480
|
+
conditions: [
|
|
1481
|
+
{
|
|
1482
|
+
field: "command",
|
|
1483
|
+
op: "matches",
|
|
1484
|
+
// The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
|
|
1485
|
+
// Python paypalrestsdk.
|
|
1486
|
+
value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
|
|
1487
|
+
}
|
|
1488
|
+
],
|
|
1489
|
+
verdict: "review",
|
|
1490
|
+
reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
name: "shield:paypal:review-code-http",
|
|
1494
|
+
tool: "bash",
|
|
1495
|
+
conditions: [
|
|
1496
|
+
{
|
|
1497
|
+
field: "command",
|
|
1498
|
+
op: "matches",
|
|
1499
|
+
// The endpoints that move money; the token call every script
|
|
1500
|
+
// starts with (/v1/oauth2/token) is not one.
|
|
1501
|
+
value: postTo(
|
|
1502
|
+
"api(?:-m)?\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/\"'`]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)"
|
|
1503
|
+
)
|
|
1504
|
+
}
|
|
1505
|
+
],
|
|
1506
|
+
verdict: "review",
|
|
1507
|
+
reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
|
|
1508
|
+
}
|
|
1509
|
+
]
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
name: "square",
|
|
1513
|
+
label: "Square",
|
|
1514
|
+
description: "Asks before money moves in Square (refunds, payments, paid orders, invoices, subscriptions), whether the agent uses a Square MCP server, the API or the Square SDK in a script. The sandbox host is never asked about, and neither are reads.",
|
|
1515
|
+
template: "finance",
|
|
1516
|
+
// Square's servers, never Squarespace (a website builder).
|
|
1517
|
+
server: "{*square,*square[-_]*,*squareup*}",
|
|
1518
|
+
mcpTools: [
|
|
1519
|
+
{
|
|
1520
|
+
// Square's official MCP server has one tool for every API call; the
|
|
1521
|
+
// service and method name what it does.
|
|
1522
|
+
name: "shield:square:review-mcp-request",
|
|
1523
|
+
tool: "{,mcp__*__}make_api_request",
|
|
1524
|
+
conditions: [
|
|
1525
|
+
{
|
|
1526
|
+
field: "service",
|
|
1527
|
+
op: "matches",
|
|
1528
|
+
value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
|
|
1529
|
+
flags: "i"
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
field: "method",
|
|
1533
|
+
op: "notMatches",
|
|
1534
|
+
value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
|
|
1535
|
+
flags: "i"
|
|
1536
|
+
}
|
|
1537
|
+
],
|
|
1538
|
+
conditionMode: "all",
|
|
1539
|
+
verdict: "review",
|
|
1540
|
+
reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1543
|
+
doors: [
|
|
1544
|
+
{
|
|
1545
|
+
name: "shield:square:review-api",
|
|
1546
|
+
tool: "bash",
|
|
1547
|
+
conditions: [
|
|
1548
|
+
{
|
|
1549
|
+
field: "command",
|
|
1550
|
+
op: "matches",
|
|
1551
|
+
// The live host only; the sandbox is connect.squareupsandbox.com.
|
|
1552
|
+
value: httpTo(
|
|
1553
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
1554
|
+
),
|
|
1555
|
+
flags: "i"
|
|
1556
|
+
},
|
|
1557
|
+
HTTP_WRITE_ANY
|
|
1558
|
+
],
|
|
1559
|
+
conditionMode: "all",
|
|
1560
|
+
verdict: "review",
|
|
1561
|
+
reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "shield:square:review-code",
|
|
1565
|
+
tool: "bash",
|
|
1566
|
+
conditions: [
|
|
1567
|
+
{
|
|
1568
|
+
field: "command",
|
|
1569
|
+
op: "matches",
|
|
1570
|
+
// The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
|
|
1571
|
+
value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
field: "command",
|
|
1575
|
+
op: "matches",
|
|
1576
|
+
value: "\\.refunds(?:Api)?\\.refund(?:Payment|_payment)\\s*\\(|\\.payments(?:Api)?\\.(?:create|complete)(?:Payment|_payment)?\\s*\\(|\\.orders(?:Api)?\\.pay(?:Order|_order)?\\s*\\(|\\.invoices(?:Api)?\\.publish(?:Invoice|_invoice)?\\s*\\("
|
|
1577
|
+
}
|
|
1578
|
+
],
|
|
1579
|
+
conditionMode: "all",
|
|
1580
|
+
verdict: "review",
|
|
1581
|
+
reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
name: "shield:square:review-code-http",
|
|
1585
|
+
tool: "bash",
|
|
1586
|
+
conditions: [
|
|
1587
|
+
{
|
|
1588
|
+
field: "command",
|
|
1589
|
+
op: "matches",
|
|
1590
|
+
value: postTo(
|
|
1591
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
1592
|
+
)
|
|
1593
|
+
}
|
|
1594
|
+
],
|
|
1595
|
+
verdict: "review",
|
|
1596
|
+
reason: "This code posts to the Square API. node9 asks before money moves (Square)."
|
|
1597
|
+
}
|
|
1598
|
+
]
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
name: "plaid",
|
|
1602
|
+
label: "Plaid",
|
|
1603
|
+
description: "Asks before money moves through Plaid (transfers, transfer authorizations, payment initiation), whether the agent uses a Plaid MCP server, the API or the Plaid SDK in a script. The sandbox host is never asked about, and neither are reads such as balances or transactions.",
|
|
1604
|
+
template: "finance",
|
|
1605
|
+
server: "*plaid*",
|
|
1606
|
+
doors: [
|
|
1607
|
+
{
|
|
1608
|
+
name: "shield:plaid:review-api",
|
|
1609
|
+
tool: "bash",
|
|
1610
|
+
conditions: [
|
|
1611
|
+
{
|
|
1612
|
+
field: "command",
|
|
1613
|
+
op: "matches",
|
|
1614
|
+
// Every Plaid endpoint is a POST, so the path is what tells a
|
|
1615
|
+
// write; the live host only, never sandbox.plaid.com. A script
|
|
1616
|
+
// that posts to the URL meets this door too.
|
|
1617
|
+
value: httpTo(
|
|
1618
|
+
"production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
|
|
1619
|
+
),
|
|
1620
|
+
flags: "i"
|
|
1621
|
+
}
|
|
1622
|
+
],
|
|
1623
|
+
verdict: "review",
|
|
1624
|
+
reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "shield:plaid:review-code",
|
|
1628
|
+
tool: "bash",
|
|
1629
|
+
conditions: [
|
|
1630
|
+
{ field: "command", op: "matches", value: "\\bplaid", flags: "i" },
|
|
1631
|
+
{
|
|
1632
|
+
field: "command",
|
|
1633
|
+
op: "matches",
|
|
1634
|
+
value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
|
|
1635
|
+
}
|
|
1636
|
+
],
|
|
1637
|
+
conditionMode: "all",
|
|
1638
|
+
verdict: "review",
|
|
1639
|
+
reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
|
|
1640
|
+
}
|
|
1641
|
+
]
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "okta",
|
|
1645
|
+
label: "Okta",
|
|
1646
|
+
description: "Asks before a user, a group member, a role, a password or a key changes in Okta, whether the agent uses an Okta MCP server, the API or the Okta SDK in a script. Reads and signing in are never asked about.",
|
|
1647
|
+
template: "identity",
|
|
1648
|
+
server: "*okta*",
|
|
1649
|
+
doors: [
|
|
1650
|
+
{
|
|
1651
|
+
name: "shield:okta:review-api",
|
|
1652
|
+
tool: "bash",
|
|
1653
|
+
conditions: [
|
|
1654
|
+
{
|
|
1655
|
+
field: "command",
|
|
1656
|
+
op: "matches",
|
|
1657
|
+
// The management API only: sign-in (/api/v1/authn, /sessions,
|
|
1658
|
+
// /oauth2) is not under these paths.
|
|
1659
|
+
value: httpTo(
|
|
1660
|
+
"[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
|
|
1661
|
+
),
|
|
1662
|
+
flags: "i"
|
|
1663
|
+
},
|
|
1664
|
+
HTTP_WRITE_ANY
|
|
1665
|
+
],
|
|
1666
|
+
conditionMode: "all",
|
|
1667
|
+
verdict: "review",
|
|
1668
|
+
reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
name: "shield:okta:review-code",
|
|
1672
|
+
tool: "bash",
|
|
1673
|
+
conditions: [
|
|
1674
|
+
{
|
|
1675
|
+
field: "command",
|
|
1676
|
+
op: "matches",
|
|
1677
|
+
// The Okta SDK in the script: a method named createUser alone is
|
|
1678
|
+
// in half the applications ever written.
|
|
1679
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
field: "command",
|
|
1683
|
+
op: "matches",
|
|
1684
|
+
value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
conditionMode: "all",
|
|
1688
|
+
verdict: "review",
|
|
1689
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
name: "shield:okta:review-code-py",
|
|
1693
|
+
tool: "bash",
|
|
1694
|
+
conditions: [
|
|
1695
|
+
{
|
|
1696
|
+
field: "command",
|
|
1697
|
+
op: "matches",
|
|
1698
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
field: "command",
|
|
1702
|
+
op: "matches",
|
|
1703
|
+
value: "\\b(?:(?:create|delete|deactivate|deactivate_or_delete|suspend|unlock|activate)_user|(?:reset|expire)_password|reset_factors|(?:add|remove)_user_(?:to|from)_group|assign_role_to_(?:user|group))\\s*\\("
|
|
1704
|
+
}
|
|
1705
|
+
],
|
|
1706
|
+
conditionMode: "all",
|
|
1707
|
+
verdict: "review",
|
|
1708
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
1709
|
+
}
|
|
1710
|
+
]
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
name: "auth0",
|
|
1714
|
+
label: "Auth0",
|
|
1715
|
+
description: "Asks before a user, a role, an application or its permissions change in Auth0, whether the agent uses an Auth0 MCP server, the auth0 CLI, the Management API or the Auth0 SDK in a script. Reads and signing in are never asked about.",
|
|
1716
|
+
template: "identity",
|
|
1717
|
+
server: "*auth0*",
|
|
1718
|
+
mcpTools: [
|
|
1719
|
+
{
|
|
1720
|
+
// Auth0's official MCP server: applications (their secrets and
|
|
1721
|
+
// callbacks), APIs and their scopes, and Actions (code that runs at
|
|
1722
|
+
// every sign-in).
|
|
1723
|
+
name: "shield:auth0:review-tenant-config",
|
|
1724
|
+
tool: "{,mcp__*__}{auth0_create_application,auth0_update_application,auth0_create_resource_server,auth0_update_resource_server,auth0_create_action,auth0_update_action,auth0_deploy_action}",
|
|
1725
|
+
conditions: [],
|
|
1726
|
+
verdict: "review",
|
|
1727
|
+
reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
|
|
1728
|
+
}
|
|
1729
|
+
],
|
|
1730
|
+
doors: [
|
|
1731
|
+
{
|
|
1732
|
+
name: "shield:auth0:review-cli",
|
|
1733
|
+
tool: "bash",
|
|
1734
|
+
conditions: [
|
|
1735
|
+
{
|
|
1736
|
+
field: "command",
|
|
1737
|
+
op: "matches",
|
|
1738
|
+
value: prog("auth0") + GAP + "\\s(?:(?:users|roles|apps|orgs)\\s+(?:create|update|delete|rm)|users\\s+roles\\s+(?:assign|add|remove|rm)|roles\\s+permissions\\s+(?:add|remove|rm)|users\\s+blocks\\s+unblock|api\\s+(?:post|put|patch|delete))(?![\\w-])",
|
|
1739
|
+
flags: "i"
|
|
1740
|
+
}
|
|
1741
|
+
],
|
|
1742
|
+
verdict: "review",
|
|
1743
|
+
reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
name: "shield:auth0:review-api",
|
|
1747
|
+
tool: "bash",
|
|
1748
|
+
conditions: [
|
|
1749
|
+
{
|
|
1750
|
+
field: "command",
|
|
1751
|
+
op: "matches",
|
|
1752
|
+
// The Management API only; /oauth/token is signing in.
|
|
1753
|
+
value: httpTo(
|
|
1754
|
+
"\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
|
|
1755
|
+
),
|
|
1756
|
+
flags: "i"
|
|
1757
|
+
},
|
|
1758
|
+
HTTP_WRITE_ANY
|
|
1759
|
+
],
|
|
1760
|
+
conditionMode: "all",
|
|
1761
|
+
verdict: "review",
|
|
1762
|
+
reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
name: "shield:auth0:review-code",
|
|
1766
|
+
tool: "bash",
|
|
1767
|
+
conditions: [
|
|
1768
|
+
{
|
|
1769
|
+
field: "command",
|
|
1770
|
+
op: "matches",
|
|
1771
|
+
// The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
|
|
1772
|
+
value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
field: "command",
|
|
1776
|
+
op: "matches",
|
|
1777
|
+
value: "\\.(?:users|roles|organizations|clients)\\.(?:create|delete|update|assignRoles|deleteRoles|assignUsers|addPermissions|deletePermissions|addMembers|deleteMembers|add_roles|remove_roles|add_users|add_permissions|remove_permissions)\\s*\\("
|
|
1778
|
+
}
|
|
1779
|
+
],
|
|
1780
|
+
conditionMode: "all",
|
|
1781
|
+
verdict: "review",
|
|
1782
|
+
reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
|
|
1783
|
+
}
|
|
1784
|
+
]
|
|
1785
|
+
}
|
|
1786
|
+
];
|
|
1787
|
+
function buildAppShield(spec) {
|
|
1788
|
+
const owner = spec.label;
|
|
1789
|
+
const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
|
|
1790
|
+
...rule,
|
|
1791
|
+
name: `shield:${spec.name}:${actionPart(rule.name)}`,
|
|
1792
|
+
server: spec.server,
|
|
1793
|
+
reason: ownReason(rule.reason, owner)
|
|
1794
|
+
}));
|
|
1795
|
+
const ownTools = (spec.mcpTools ?? []).map((rule) => ({
|
|
1796
|
+
...rule,
|
|
1797
|
+
server: spec.server
|
|
1798
|
+
}));
|
|
1799
|
+
return {
|
|
1800
|
+
name: spec.name,
|
|
1801
|
+
description: spec.description,
|
|
1802
|
+
aliases: [],
|
|
1803
|
+
smartRules: [...toolRules, ...ownTools, ...spec.doors],
|
|
1804
|
+
dangerousWords: []
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
var APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
|
|
1808
|
+
var APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
|
|
1809
|
+
var APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
|
|
1810
|
+
function buildGeneralRules() {
|
|
1811
|
+
const out = [];
|
|
1812
|
+
for (const name of Object.keys(ACTION_TEMPLATES)) {
|
|
1813
|
+
const { slug, label } = ACTION_CHECKS[name];
|
|
1814
|
+
for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
|
|
1815
|
+
const part = actionPart(rule.name).replace(/^review-/, "");
|
|
1816
|
+
out.push({
|
|
1817
|
+
...rule,
|
|
1818
|
+
name: `review-action-${slug}-${part}`,
|
|
1819
|
+
notServer: APP_SERVER_GLOB,
|
|
1820
|
+
reason: ownReason(rule.reason, label)
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return out;
|
|
1825
|
+
}
|
|
1826
|
+
var GENERAL_ACTION_RULES = buildGeneralRules();
|
|
1827
|
+
function generalActionRuleNames(template) {
|
|
1828
|
+
const { slug } = ACTION_CHECKS[template];
|
|
1829
|
+
return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug}-`)).map(
|
|
1830
|
+
(r) => r.name
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1273
1833
|
function validateShieldDefinition(raw) {
|
|
1274
1834
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1275
1835
|
return { error: "Shield file is not an object" };
|
|
@@ -1288,36 +1848,36 @@ var BUILTIN_SHIELDS = {
|
|
|
1288
1848
|
[bash_safe_default.name]: bash_safe_default,
|
|
1289
1849
|
[docker_default.name]: docker_default,
|
|
1290
1850
|
[filesystem_default.name]: filesystem_default,
|
|
1291
|
-
// Dangerous actions as category shields (node9Firewall#385): an MCP app's
|
|
1292
|
-
// money, credential and publishing tools, asked about whatever the request
|
|
1293
|
-
// said. One glob per rule covers the bare name the gateway sends and the
|
|
1294
|
-
// mcp__<server>__ name the agent hook sends.
|
|
1295
|
-
[finance_default.name]: finance_default,
|
|
1296
1851
|
[github_default.name]: github_default,
|
|
1297
|
-
[identity_default.name]: identity_default,
|
|
1298
1852
|
[k8s_default.name]: k8s_default,
|
|
1299
1853
|
[mongodb_default.name]: mongodb_default,
|
|
1300
1854
|
[postgres_default.name]: postgres_default,
|
|
1301
1855
|
[project_jail_default.name]: project_jail_default,
|
|
1302
|
-
[
|
|
1303
|
-
|
|
1856
|
+
[redis_default.name]: redis_default,
|
|
1857
|
+
// App shields generated from the action templates (node9Firewall#390): one
|
|
1858
|
+
// per app, every door (MCP server, CLI, API, library), off by default.
|
|
1859
|
+
...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
|
|
1304
1860
|
};
|
|
1305
|
-
var ACTION_SHIELD_NAMES = /* @__PURE__ */ new Set([
|
|
1306
|
-
"finance",
|
|
1307
|
-
"identity",
|
|
1308
|
-
"publishing"
|
|
1309
|
-
]);
|
|
1310
1861
|
function assertBuiltinShieldRegexesAreSafe() {
|
|
1311
|
-
|
|
1862
|
+
const sets = [
|
|
1863
|
+
...Object.values(BUILTIN_SHIELDS).map((sh) => ({
|
|
1864
|
+
name: sh.name,
|
|
1865
|
+
smartRules: sh.smartRules
|
|
1866
|
+
})),
|
|
1867
|
+
...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
|
|
1868
|
+
{ name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
|
|
1869
|
+
];
|
|
1870
|
+
for (const shield of sets) {
|
|
1312
1871
|
for (const rule of shield.smartRules) {
|
|
1313
1872
|
const conditions = rule.conditions ?? [];
|
|
1314
1873
|
for (const cond of conditions) {
|
|
1315
1874
|
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
1316
1875
|
const pattern = cond.value;
|
|
1317
1876
|
if (!pattern) continue;
|
|
1318
|
-
|
|
1877
|
+
const problem = validateRegex(pattern);
|
|
1878
|
+
if (problem) {
|
|
1319
1879
|
throw new Error(
|
|
1320
|
-
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has
|
|
1880
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
|
|
1321
1881
|
);
|
|
1322
1882
|
}
|
|
1323
1883
|
}
|
|
@@ -1348,9 +1908,9 @@ var CHECK_TEXT = {
|
|
|
1348
1908
|
},
|
|
1349
1909
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
1350
1910
|
"commands.inline-exec": {
|
|
1351
|
-
plain: "The agent runs a small program written inside the command itself.",
|
|
1911
|
+
plain: "The agent runs a small program written inside the command itself, and node9 reads it: ordinary code runs, code that deletes, sends data out, runs other commands or reads protected files is held, and so is code node9 cannot read.",
|
|
1352
1912
|
example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
|
|
1353
|
-
advice: "
|
|
1913
|
+
advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
|
|
1354
1914
|
},
|
|
1355
1915
|
"commands.eval-dynamic": {
|
|
1356
1916
|
plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
|
|
@@ -1382,6 +1942,21 @@ var CHECK_TEXT = {
|
|
|
1382
1942
|
example: "chmod 777 deploy.sh",
|
|
1383
1943
|
advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
|
|
1384
1944
|
},
|
|
1945
|
+
"actions.money": {
|
|
1946
|
+
plain: "The agent moves money (a refund, a payout, a transfer, a charge) through an app node9 has no protection for, such as an internal billing server.",
|
|
1947
|
+
example: "refund_order on a server called acme-billing",
|
|
1948
|
+
advice: "Keep Review. When the app is Stripe or PayPal, turn on its own protection instead: it covers the CLI, the API and scripts too, and a call from it no longer comes here."
|
|
1949
|
+
},
|
|
1950
|
+
"actions.access": {
|
|
1951
|
+
plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
|
|
1952
|
+
example: "invite_user or grant_role on a company admin server",
|
|
1953
|
+
advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
|
|
1954
|
+
},
|
|
1955
|
+
"actions.publish": {
|
|
1956
|
+
plain: "The agent publishes something outside the organisation (a release, a package, a deploy, a public post) through an app node9 has no protection for.",
|
|
1957
|
+
example: "create_release or deploy_to_production on a company MCP server",
|
|
1958
|
+
advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
|
|
1959
|
+
},
|
|
1385
1960
|
"commands.sudo": {
|
|
1386
1961
|
plain: "The agent runs a command as the administrator (root).",
|
|
1387
1962
|
example: "sudo apt-get install -y nginx",
|
|
@@ -1542,11 +2117,32 @@ var CHECK_TEXT = {
|
|
|
1542
2117
|
advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
|
|
1543
2118
|
},
|
|
1544
2119
|
"loading.malicious-package": {
|
|
1545
|
-
plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
|
|
2120
|
+
plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only. A new release built from the same repository as the one before it (registry provenance) and an install script the package has always run do not stop; a release that lost the provenance its previous release had does.",
|
|
1546
2121
|
example: "npm install of a package flagged as malware",
|
|
1547
2122
|
advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
|
|
1548
2123
|
}
|
|
1549
2124
|
};
|
|
2125
|
+
var LEVELS = ["audit", "soft", "standard", "strict", "block-all"];
|
|
2126
|
+
var MODE_VALUES = ["observe", ...LEVELS];
|
|
2127
|
+
var STRICT = {
|
|
2128
|
+
"commands.sql-ddl": "block",
|
|
2129
|
+
"commands.disk-destroy": "block",
|
|
2130
|
+
"commands.node9-disarm": "block",
|
|
2131
|
+
"files.system-dirs": "block",
|
|
2132
|
+
"files.shell-startup": "block",
|
|
2133
|
+
"files.password-hashes": "block",
|
|
2134
|
+
"data.secrets-weak": "block",
|
|
2135
|
+
"data.pipe-chain": "block",
|
|
2136
|
+
"network.unknown-host": "review",
|
|
2137
|
+
"network.internal-addresses": "block",
|
|
2138
|
+
"behavior.prompt-injection": "log",
|
|
2139
|
+
"loading.skill-tamper": "block"
|
|
2140
|
+
};
|
|
2141
|
+
var BLOCK_ALL = {
|
|
2142
|
+
...STRICT,
|
|
2143
|
+
"commands.unknown": "review",
|
|
2144
|
+
"commands.temp-binary": "block"
|
|
2145
|
+
};
|
|
1550
2146
|
var VERDICTS = ["off", "log", "review", "block"];
|
|
1551
2147
|
var ALL = VERDICTS;
|
|
1552
2148
|
var BUILTIN_DANGEROUS_WORDS = ["mkfs", "shred"];
|
|
@@ -1593,7 +2189,7 @@ var PRODUCT_CHECKS = [
|
|
|
1593
2189
|
id: "commands.inline-exec",
|
|
1594
2190
|
group: "commands",
|
|
1595
2191
|
title: "Inline code execution",
|
|
1596
|
-
catches: "
|
|
2192
|
+
catches: "inline code (python -c, node -e, a heredoc into an interpreter) that deletes, sends, opens a socket, runs commands, reads protected files or decodes and runs; unreadable inline code",
|
|
1597
2193
|
values: ALL,
|
|
1598
2194
|
defaultValue: "review",
|
|
1599
2195
|
legacy: ["Node9 Standard (Inline Execution)"]
|
|
@@ -1950,6 +2546,38 @@ var PRODUCT_CHECKS = [
|
|
|
1950
2546
|
values: ALL,
|
|
1951
2547
|
defaultValue: "block",
|
|
1952
2548
|
legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
|
|
2549
|
+
},
|
|
2550
|
+
// ── Actions in apps (node9Firewall#390) ──────────────────────────────────
|
|
2551
|
+
// The safety net under the app shields: an action that moves money, changes
|
|
2552
|
+
// access or publishes, on an MCP server no app shield claims (an internal
|
|
2553
|
+
// billing server, a vendor with no shield, Zapier). Review by default; the
|
|
2554
|
+
// rules come from the action templates (shields/apps.ts).
|
|
2555
|
+
{
|
|
2556
|
+
id: "actions.money",
|
|
2557
|
+
group: "actions",
|
|
2558
|
+
title: "Money moves",
|
|
2559
|
+
catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
|
|
2560
|
+
values: ALL,
|
|
2561
|
+
defaultValue: "review",
|
|
2562
|
+
legacy: generalActionRuleNames("finance")
|
|
2563
|
+
},
|
|
2564
|
+
{
|
|
2565
|
+
id: "actions.access",
|
|
2566
|
+
group: "actions",
|
|
2567
|
+
title: "Accounts and access change",
|
|
2568
|
+
catches: "a password, key, role, permission or user changed through an app with no protection of its own",
|
|
2569
|
+
values: ALL,
|
|
2570
|
+
defaultValue: "review",
|
|
2571
|
+
legacy: generalActionRuleNames("identity")
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
id: "actions.publish",
|
|
2575
|
+
group: "actions",
|
|
2576
|
+
title: "Something is published",
|
|
2577
|
+
catches: "a release, package, deploy or public post through an app with no protection of its own",
|
|
2578
|
+
values: ALL,
|
|
2579
|
+
defaultValue: "review",
|
|
2580
|
+
legacy: generalActionRuleNames("publishing")
|
|
1953
2581
|
}
|
|
1954
2582
|
];
|
|
1955
2583
|
var SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
|
|
@@ -2071,6 +2699,170 @@ var HOMOGLYPHS = {
|
|
|
2071
2699
|
\u03A7: "X"
|
|
2072
2700
|
};
|
|
2073
2701
|
var HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
2702
|
+
var SYSTEM_DIRS = [
|
|
2703
|
+
"/usr",
|
|
2704
|
+
"/etc",
|
|
2705
|
+
"/boot",
|
|
2706
|
+
"/bin",
|
|
2707
|
+
"/sbin",
|
|
2708
|
+
"/lib",
|
|
2709
|
+
"/lib64",
|
|
2710
|
+
"/var/log"
|
|
2711
|
+
];
|
|
2712
|
+
var UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
2713
|
+
var LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
2714
|
+
var MAX_INLINE_CODE = 64 * 1024;
|
|
2715
|
+
var OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
|
|
2716
|
+
var pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
|
|
2717
|
+
var either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
|
|
2718
|
+
var PYTHON = [
|
|
2719
|
+
[
|
|
2720
|
+
"deletes",
|
|
2721
|
+
/\bos\.(?:remove|unlink|rmdir|removedirs)\s*\(|\bshutil\.rmtree\s*\(|\.(?:unlink|rmdir)\s*\(|\bfrom\s+(?:os|shutil)\s+import\s+[^\n]*\b(?:remove|unlink|rmdir|removedirs|rmtree)\b/
|
|
2722
|
+
],
|
|
2723
|
+
[
|
|
2724
|
+
"writes-outside",
|
|
2725
|
+
new RegExp(
|
|
2726
|
+
String.raw`\bopen\s*\(\s*[rRbBuU]?['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*(?:mode\s*=\s*)?[rRbB]?['"][^'"\n]*[wax+]|\bPath\s*\(\s*[rR]?['"]${OUTSIDE}[^'"\n]*['"]\s*\)\s*\.(?:write_text|write_bytes|touch|mkdir|symlink_to)\s*\(|\bshutil\.(?:copy\w*|move)\s*\([^)\n]*['"]${OUTSIDE}|\bos\.(?:rename|replace|symlink|link|truncate|makedirs|mkdir)\s*\([^)\n]*['"]${OUTSIDE}`
|
|
2727
|
+
)
|
|
2728
|
+
],
|
|
2729
|
+
[
|
|
2730
|
+
"sends",
|
|
2731
|
+
either(
|
|
2732
|
+
pyImport(
|
|
2733
|
+
String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
|
|
2734
|
+
),
|
|
2735
|
+
/\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
|
|
2736
|
+
)
|
|
2737
|
+
],
|
|
2738
|
+
[
|
|
2739
|
+
"listens",
|
|
2740
|
+
either(
|
|
2741
|
+
pyImport(String.raw`pty|socketserver|http\.server`),
|
|
2742
|
+
/\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
|
|
2743
|
+
)
|
|
2744
|
+
],
|
|
2745
|
+
[
|
|
2746
|
+
"runs-commands",
|
|
2747
|
+
either(
|
|
2748
|
+
pyImport("subprocess|pexpect|commands|psutil"),
|
|
2749
|
+
/\bos\.(?:system|popen|exec\w*|spawn\w*|posix_spawn\w*|fork|forkpty|kill|killpg)\s*\(|\bfrom\s+os\s+import\s+[^\n;]*\b(?:system|popen|exec\w*|spawn\w*|fork|kill)\b|\bfrom\s+(?:os|shutil|pty|ctypes)\s+import\s+\*/
|
|
2750
|
+
)
|
|
2751
|
+
],
|
|
2752
|
+
[
|
|
2753
|
+
"builds-code",
|
|
2754
|
+
/(?<![\w.])(?:exec|eval|compile|execfile)\s*\(|\b__import__\s*\(|\bimportlib\b|\b(?:c?pickle|dill|marshal|shelve)\.loads?\s*\(|\byaml\.(?:unsafe_)?load\s*\((?![^)\n]*SafeLoader)|\bctypes\b|\b(?:getattr|vars)\s*\(\s*(?:os|subprocess|shutil|socket|sys|pty|builtins|__builtins__)\b|\b(?:os|subprocess|shutil|socket|sys|pty|builtins)\.__dict__\b|\b__builtins__\b|\bsys\.modules\b|\bglobals\s*\(\s*\)\s*\[/
|
|
2755
|
+
],
|
|
2756
|
+
[
|
|
2757
|
+
// Spyware shapes: a screenshot, the clipboard, a key logger.
|
|
2758
|
+
"monitors",
|
|
2759
|
+
either(
|
|
2760
|
+
pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
|
|
2761
|
+
/\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
|
|
2762
|
+
)
|
|
2763
|
+
],
|
|
2764
|
+
[
|
|
2765
|
+
"permissions",
|
|
2766
|
+
/\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
|
|
2767
|
+
]
|
|
2768
|
+
];
|
|
2769
|
+
var NODE = [
|
|
2770
|
+
[
|
|
2771
|
+
"deletes",
|
|
2772
|
+
/\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
|
|
2773
|
+
],
|
|
2774
|
+
[
|
|
2775
|
+
"writes-outside",
|
|
2776
|
+
new RegExp(
|
|
2777
|
+
String.raw`(?:\.(?:writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream|copyFile|copyFileSync|cp|cpSync|rename|renameSync|truncate|truncateSync|symlink|symlinkSync|mkdir|mkdirSync)|\bDeno\.(?:writeTextFile|writeFile|rename|mkdir)(?:Sync)?|\bBun\.write)\s*\(\s*['"\x60]${OUTSIDE}`
|
|
2778
|
+
)
|
|
2779
|
+
],
|
|
2780
|
+
[
|
|
2781
|
+
"sends",
|
|
2782
|
+
/\bfetch\s*\(|(?:\brequire\s*\(\s*|\bfrom\s+|\bimport\s+)['"\x60](?:node:)?(?:http|https|http2|net|dgram|tls|axios|node-fetch|ws|request|got|undici|nodemailer)['"\x60]|\bnew\s+WebSocket\b|\bXMLHttpRequest\b|\b(?:Deno|Bun)\.connect\w*\s*\(/
|
|
2783
|
+
],
|
|
2784
|
+
["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
|
|
2785
|
+
[
|
|
2786
|
+
"runs-commands",
|
|
2787
|
+
/\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
|
|
2788
|
+
],
|
|
2789
|
+
[
|
|
2790
|
+
"builds-code",
|
|
2791
|
+
/(?<![\w.])eval\s*\(|\bnew\s+Function\s*\(|(?<![\w.])Function\s*\(|(?:\brequire\s*\(\s*|\bfrom\s+)['"\x60](?:node:)?vm['"\x60]|\bvm\.\w+|\brequire\s*\(\s*[^'"\x60\s)]|\bimport\s*\(\s*[^'"\x60\s)]/
|
|
2792
|
+
],
|
|
2793
|
+
[
|
|
2794
|
+
"permissions",
|
|
2795
|
+
/\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
|
|
2796
|
+
]
|
|
2797
|
+
];
|
|
2798
|
+
var PERL = [
|
|
2799
|
+
["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
|
|
2800
|
+
[
|
|
2801
|
+
"writes-outside",
|
|
2802
|
+
new RegExp(
|
|
2803
|
+
String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
|
|
2804
|
+
)
|
|
2805
|
+
],
|
|
2806
|
+
[
|
|
2807
|
+
"sends",
|
|
2808
|
+
/\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
|
|
2809
|
+
],
|
|
2810
|
+
[
|
|
2811
|
+
"runs-commands",
|
|
2812
|
+
/\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
|
|
2813
|
+
],
|
|
2814
|
+
["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
|
|
2815
|
+
["permissions", /\b(?:chmod|chown)\b/]
|
|
2816
|
+
];
|
|
2817
|
+
var RUBY = [
|
|
2818
|
+
[
|
|
2819
|
+
"deletes",
|
|
2820
|
+
/\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
|
|
2821
|
+
],
|
|
2822
|
+
[
|
|
2823
|
+
"writes-outside",
|
|
2824
|
+
new RegExp(
|
|
2825
|
+
String.raw`\bFile\.(?:write|binwrite|symlink|rename)\s*\(?\s*['"]${OUTSIDE}|\bFile\.open\s*\(?\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][wa+]|\bFileUtils\.(?:cp\w*|mv|install|ln\w*|mkdir_p|touch)\s*\(?[^\n]*['"]${OUTSIDE}`
|
|
2826
|
+
)
|
|
2827
|
+
],
|
|
2828
|
+
[
|
|
2829
|
+
"sends",
|
|
2830
|
+
/\bNet::\w+|\b(?:TCPSocket|UDPSocket|UNIXSocket|Socket)\b|\bopen-uri\b|\bURI\.open\b|\b(?:HTTParty|Faraday|RestClient|Excon|Typhoeus)\b|\brequire\s+['"](?:socket|net\/\w+|open-uri)['"]/
|
|
2831
|
+
],
|
|
2832
|
+
["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
|
|
2833
|
+
[
|
|
2834
|
+
"runs-commands",
|
|
2835
|
+
/(?<![\w.])(?:system|exec|spawn|fork)\b|`[^`]*`|%x[{([|!]|(?<![\w.])open\s*\(?\s*['"]\||\bIO\.popen\b|\bOpen3\b|\bProcess\.(?:spawn|kill|exec)\b|\bKernel\.(?:system|exec|spawn|open)\b/
|
|
2836
|
+
],
|
|
2837
|
+
[
|
|
2838
|
+
"builds-code",
|
|
2839
|
+
/(?<![\w.])eval\b|\b(?:instance|class|module)_eval\b|\b(?:send|public_send)\s*\(?\s*:?['"]?(?:system|eval|exec|spawn)\b|\bMarshal\.load\b|\bYAML\.(?:unsafe_)?load\b|\bconst_get\b/
|
|
2840
|
+
],
|
|
2841
|
+
["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
|
|
2842
|
+
];
|
|
2843
|
+
var PHP = [
|
|
2844
|
+
["deletes", /\b(?:unlink|rmdir)\s*\(/],
|
|
2845
|
+
[
|
|
2846
|
+
"writes-outside",
|
|
2847
|
+
new RegExp(
|
|
2848
|
+
String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
|
|
2849
|
+
)
|
|
2850
|
+
],
|
|
2851
|
+
[
|
|
2852
|
+
"sends",
|
|
2853
|
+
/\b(?:curl_exec|curl_init|fsockopen|pfsockopen|stream_socket_client|socket_create|socket_connect|mail|ftp_connect)\s*\(|\b(?:file_get_contents|fopen|file|readfile)\s*\(\s*['"](?:https?|ftp):/
|
|
2854
|
+
],
|
|
2855
|
+
["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
|
|
2856
|
+
[
|
|
2857
|
+
"runs-commands",
|
|
2858
|
+
/\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
|
|
2859
|
+
],
|
|
2860
|
+
[
|
|
2861
|
+
"builds-code",
|
|
2862
|
+
/\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
|
|
2863
|
+
],
|
|
2864
|
+
["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
|
|
2865
|
+
];
|
|
2074
2866
|
var { syntax } = mvdanSh;
|
|
2075
2867
|
var sharedParser = syntax.NewParser();
|
|
2076
2868
|
var AST_CACHE_MAX = 5e3;
|
|
@@ -2646,18 +3438,6 @@ var DEFAULT_OPS = new Set(
|
|
|
2646
3438
|
(s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
|
|
2647
3439
|
)
|
|
2648
3440
|
);
|
|
2649
|
-
var SYSTEM_DIRS = [
|
|
2650
|
-
"/usr",
|
|
2651
|
-
"/etc",
|
|
2652
|
-
"/boot",
|
|
2653
|
-
"/bin",
|
|
2654
|
-
"/sbin",
|
|
2655
|
-
"/lib",
|
|
2656
|
-
"/lib64",
|
|
2657
|
-
"/var/log"
|
|
2658
|
-
];
|
|
2659
|
-
var UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
2660
|
-
var LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
2661
3441
|
var BIP39_ENGLISH = `abandon
|
|
2662
3442
|
ability
|
|
2663
3443
|
able
|
|
@@ -5718,14 +6498,14 @@ var SENSITIVE_PATH_REGEXES = [
|
|
|
5718
6498
|
];
|
|
5719
6499
|
function assertBuiltinPatternsAreSafe() {
|
|
5720
6500
|
for (const p of DLP_PATTERNS) {
|
|
5721
|
-
if (!
|
|
6501
|
+
if (!safeRegex2(p.regex.source)) {
|
|
5722
6502
|
throw new Error(
|
|
5723
6503
|
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
5724
6504
|
);
|
|
5725
6505
|
}
|
|
5726
6506
|
}
|
|
5727
6507
|
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
5728
|
-
if (!
|
|
6508
|
+
if (!safeRegex2(re.source)) {
|
|
5729
6509
|
throw new Error(
|
|
5730
6510
|
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
5731
6511
|
);
|
|
@@ -5835,7 +6615,11 @@ var MAP_GOVERNED = /* @__PURE__ */ new Set([
|
|
|
5835
6615
|
"commands.disk-destroy",
|
|
5836
6616
|
"commands.dangerous-word",
|
|
5837
6617
|
"commands.node9-disarm",
|
|
5838
|
-
"data.pipe-chain"
|
|
6618
|
+
"data.pipe-chain",
|
|
6619
|
+
// The general action checks (node9Firewall#390): smart rules the map governs.
|
|
6620
|
+
"actions.money",
|
|
6621
|
+
"actions.access",
|
|
6622
|
+
"actions.publish"
|
|
5839
6623
|
]);
|
|
5840
6624
|
var KNOB_GOVERNED = {
|
|
5841
6625
|
"data.secrets": ["off", "block"],
|
|
@@ -5876,6 +6660,21 @@ var PUBLISH = {
|
|
|
5876
6660
|
var OWN_HEADS = /* @__PURE__ */ new Set([...Object.keys(PUBLISH), "git", "gh"]);
|
|
5877
6661
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
5878
6662
|
|
|
6663
|
+
// src/shields/build.ts
|
|
6664
|
+
var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
|
|
6665
|
+
function withAccessReading(def) {
|
|
6666
|
+
let changed = false;
|
|
6667
|
+
const smartRules = def.smartRules.map((r) => {
|
|
6668
|
+
const [c, ...rest] = r.conditions ?? [];
|
|
6669
|
+
if (rest.length > 0 || !c || r.tool !== "bash" || !LEGACY_BASH_PATH_RULE.test(r.name ?? "") || c.field !== "command" || c.op !== "matches" || c.reading !== void 0) {
|
|
6670
|
+
return r;
|
|
6671
|
+
}
|
|
6672
|
+
changed = true;
|
|
6673
|
+
return { ...r, conditions: [{ ...c, reading: "access" }] };
|
|
6674
|
+
});
|
|
6675
|
+
return changed ? { ...def, smartRules } : def;
|
|
6676
|
+
}
|
|
6677
|
+
|
|
5879
6678
|
// src/shields.ts
|
|
5880
6679
|
var USER_SHIELDS_DIR = path.join(os.homedir(), ".node9", "shields");
|
|
5881
6680
|
function loadUserShields() {
|
|
@@ -5902,7 +6701,7 @@ function loadUserShields() {
|
|
|
5902
6701
|
`);
|
|
5903
6702
|
continue;
|
|
5904
6703
|
}
|
|
5905
|
-
result[v.ok.name] = v.ok;
|
|
6704
|
+
result[v.ok.name] = withAccessReading(v.ok);
|
|
5906
6705
|
} catch (err) {
|
|
5907
6706
|
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
5908
6707
|
`);
|
|
@@ -5921,6 +6720,268 @@ var TUNING_FIELDS_BY_NAME = new Map(TUNING_FIELDS.map((f) => [`${f.checkId}.${f.
|
|
|
5921
6720
|
|
|
5922
6721
|
// src/config/index.ts
|
|
5923
6722
|
var DANGEROUS_WORDS = [...BUILTIN_DANGEROUS_WORDS];
|
|
6723
|
+
var DEFAULT_CONFIG = {
|
|
6724
|
+
version: "1.0",
|
|
6725
|
+
policySource: "local",
|
|
6726
|
+
ssrfStrictSource: "default",
|
|
6727
|
+
settings: {
|
|
6728
|
+
mode: "standard",
|
|
6729
|
+
autoStartDaemon: true,
|
|
6730
|
+
// Off by default: this is a DEBUG facility, not part of the product's job.
|
|
6731
|
+
// When on, every tool call appends a row to ~/.node9/hook-debug.log, which
|
|
6732
|
+
// has no production reader and no size ceiling. Turn it on deliberately
|
|
6733
|
+
// (this setting, or NODE9_DEBUG=1) while investigating something.
|
|
6734
|
+
// Error breadcrumbs written from catch blocks are NOT gated by this and
|
|
6735
|
+
// keep working when it is off — see CLAUDE.md "Always write to
|
|
6736
|
+
// hook-debug.log in catch blocks that guard audit trail".
|
|
6737
|
+
enableHookLogDebug: false,
|
|
6738
|
+
approvalTimeoutMs: 12e4,
|
|
6739
|
+
// 120-second auto-deny timeout
|
|
6740
|
+
flightRecorder: true,
|
|
6741
|
+
auditHashArgs: true,
|
|
6742
|
+
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
6743
|
+
cloudSyncIntervalHours: 5,
|
|
6744
|
+
shipper: { enabled: true, intervalSeconds: 20 }
|
|
6745
|
+
},
|
|
6746
|
+
policy: {
|
|
6747
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
6748
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
6749
|
+
ignoredTools: [
|
|
6750
|
+
"list_*",
|
|
6751
|
+
"get_*",
|
|
6752
|
+
"read_*",
|
|
6753
|
+
"describe_*",
|
|
6754
|
+
"read",
|
|
6755
|
+
"glob",
|
|
6756
|
+
"grep",
|
|
6757
|
+
"ls",
|
|
6758
|
+
"notebookread",
|
|
6759
|
+
"notebookedit",
|
|
6760
|
+
"webfetch",
|
|
6761
|
+
"websearch",
|
|
6762
|
+
"exitplanmode",
|
|
6763
|
+
"askuserquestion",
|
|
6764
|
+
"agent",
|
|
6765
|
+
"task*",
|
|
6766
|
+
"toolsearch",
|
|
6767
|
+
"mcp__ide__*",
|
|
6768
|
+
"getDiagnostics"
|
|
6769
|
+
],
|
|
6770
|
+
toolInspection: {
|
|
6771
|
+
bash: "command",
|
|
6772
|
+
shell: "command",
|
|
6773
|
+
run_shell_command: "command",
|
|
6774
|
+
"terminal.execute": "command",
|
|
6775
|
+
"postgres:query": "sql"
|
|
6776
|
+
},
|
|
6777
|
+
smartRules: [
|
|
6778
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
6779
|
+
{
|
|
6780
|
+
name: "block-rm-rf-home",
|
|
6781
|
+
tool: "bash",
|
|
6782
|
+
conditionMode: "all",
|
|
6783
|
+
conditions: [
|
|
6784
|
+
{
|
|
6785
|
+
field: "command",
|
|
6786
|
+
op: "matches",
|
|
6787
|
+
// Anchor rm as a shell command (not inside a string arg like a git commit message).
|
|
6788
|
+
value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
6789
|
+
},
|
|
6790
|
+
{
|
|
6791
|
+
field: "command",
|
|
6792
|
+
op: "matches",
|
|
6793
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
6794
|
+
}
|
|
6795
|
+
],
|
|
6796
|
+
verdict: "block",
|
|
6797
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
6798
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
6799
|
+
},
|
|
6800
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
6801
|
+
{
|
|
6802
|
+
name: "no-delete-without-where",
|
|
6803
|
+
tool: "*",
|
|
6804
|
+
conditions: [
|
|
6805
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
6806
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
6807
|
+
],
|
|
6808
|
+
conditionMode: "all",
|
|
6809
|
+
verdict: "review",
|
|
6810
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
6811
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
6812
|
+
},
|
|
6813
|
+
{
|
|
6814
|
+
name: "review-drop-truncate-shell",
|
|
6815
|
+
tool: "bash",
|
|
6816
|
+
conditions: [
|
|
6817
|
+
{
|
|
6818
|
+
field: "command",
|
|
6819
|
+
op: "matches",
|
|
6820
|
+
// Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
|
|
6821
|
+
value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
|
|
6822
|
+
flags: "i"
|
|
6823
|
+
},
|
|
6824
|
+
{
|
|
6825
|
+
field: "command",
|
|
6826
|
+
op: "matches",
|
|
6827
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
6828
|
+
flags: "i"
|
|
6829
|
+
}
|
|
6830
|
+
],
|
|
6831
|
+
conditionMode: "all",
|
|
6832
|
+
verdict: "review",
|
|
6833
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
6834
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
6835
|
+
},
|
|
6836
|
+
// ── node9 itself ──────────────────────────────────────────────────────
|
|
6837
|
+
// The agent runs as the user, so a `node9 ...` command from Bash can
|
|
6838
|
+
// clear the session's taint or exposure, trust a destination or pause
|
|
6839
|
+
// the gate. Those are the user's own commands; from the agent they are
|
|
6840
|
+
// a review (fw#318 review finding 1).
|
|
6841
|
+
{
|
|
6842
|
+
name: "review-node9-disarm",
|
|
6843
|
+
tool: "bash",
|
|
6844
|
+
conditions: [
|
|
6845
|
+
{
|
|
6846
|
+
field: "command",
|
|
6847
|
+
op: "matches",
|
|
6848
|
+
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
6849
|
+
// fails the engine's ReDoS check and the rule would be dropped.
|
|
6850
|
+
// `apps`: turning an app off, allowing one of its actions, or
|
|
6851
|
+
// linking a server to an app (the general checks leave it).
|
|
6852
|
+
value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|apps (link|[a-z0-9-]{1,32} (off|[a-z]{1,32} allow))|egress allow|approver set)\\b",
|
|
6853
|
+
flags: "i"
|
|
6854
|
+
}
|
|
6855
|
+
],
|
|
6856
|
+
conditionMode: "all",
|
|
6857
|
+
verdict: "review",
|
|
6858
|
+
reason: "node9 self-weakening command run by the agent",
|
|
6859
|
+
description: "The AI wants to run a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield. These are your commands to run, not the agent's."
|
|
6860
|
+
},
|
|
6861
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
6862
|
+
{
|
|
6863
|
+
name: "review-force-push",
|
|
6864
|
+
tool: "bash",
|
|
6865
|
+
conditions: [
|
|
6866
|
+
{
|
|
6867
|
+
field: "command",
|
|
6868
|
+
op: "matches",
|
|
6869
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
6870
|
+
// "git push --force" as a string don't false-positive.
|
|
6871
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
|
|
6872
|
+
flags: "i"
|
|
6873
|
+
}
|
|
6874
|
+
],
|
|
6875
|
+
conditionMode: "all",
|
|
6876
|
+
verdict: "review",
|
|
6877
|
+
reason: "Force push rewrites remote history \u2014 confirm this is intentional",
|
|
6878
|
+
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
6879
|
+
},
|
|
6880
|
+
{
|
|
6881
|
+
name: "review-git-destructive",
|
|
6882
|
+
tool: "bash",
|
|
6883
|
+
conditions: [
|
|
6884
|
+
{
|
|
6885
|
+
field: "command",
|
|
6886
|
+
op: "matches",
|
|
6887
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
6888
|
+
// "git reset --hard" as a string don't false-positive.
|
|
6889
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
6890
|
+
flags: "i"
|
|
6891
|
+
},
|
|
6892
|
+
{
|
|
6893
|
+
field: "command",
|
|
6894
|
+
op: "notMatches",
|
|
6895
|
+
// Exclude recovery ops and routine branch-surgery (--onto) — these are not destructive.
|
|
6896
|
+
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
|
|
6897
|
+
flags: "i"
|
|
6898
|
+
}
|
|
6899
|
+
],
|
|
6900
|
+
conditionMode: "all",
|
|
6901
|
+
verdict: "review",
|
|
6902
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
6903
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
6904
|
+
},
|
|
6905
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
6906
|
+
{
|
|
6907
|
+
name: "review-sudo",
|
|
6908
|
+
tool: "bash",
|
|
6909
|
+
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
6910
|
+
conditionMode: "all",
|
|
6911
|
+
verdict: "review",
|
|
6912
|
+
reason: "Command requires elevated privileges",
|
|
6913
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
6914
|
+
},
|
|
6915
|
+
{
|
|
6916
|
+
name: "review-curl-pipe-shell",
|
|
6917
|
+
tool: "bash",
|
|
6918
|
+
conditions: [
|
|
6919
|
+
{
|
|
6920
|
+
field: "command",
|
|
6921
|
+
op: "matches",
|
|
6922
|
+
// Anchor curl/wget as a shell command so node -e scripts testing this
|
|
6923
|
+
// regex pattern don't self-match as a false positive.
|
|
6924
|
+
// Downloader and sink groups mirror the engine's DOWNLOAD_CMDS and
|
|
6925
|
+
// SHELL_INTERPRETERS; pipe-to-shell-vocabulary-drift.spec.ts fails if
|
|
6926
|
+
// either list gains a name this regex does not. The PowerShell sinks
|
|
6927
|
+
// carry \b so `iex` cannot match inside e.g. `iexplore`, and so does
|
|
6928
|
+
// `sh`, so a quoted body containing `| Shipment` is not a shell
|
|
6929
|
+
// (fw#372 X3, node9Firewall#332).
|
|
6930
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh\\b|(?:iex|invoke-expression|powershell|pwsh)\\b)",
|
|
6931
|
+
flags: "i"
|
|
6932
|
+
}
|
|
6933
|
+
],
|
|
6934
|
+
conditionMode: "all",
|
|
6935
|
+
verdict: "block",
|
|
6936
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
6937
|
+
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
6938
|
+
},
|
|
6939
|
+
// ── Actions in apps (node9Firewall#390) ───────────────────────────────
|
|
6940
|
+
// actions.money / actions.access / actions.publish: the action templates
|
|
6941
|
+
// on every MCP server no app shield claims, plus the shell publishing
|
|
6942
|
+
// rules. Review by default; the check value governs them.
|
|
6943
|
+
...GENERAL_ACTION_RULES
|
|
6944
|
+
],
|
|
6945
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
|
|
6946
|
+
egress: {
|
|
6947
|
+
enabled: false,
|
|
6948
|
+
mode: "review",
|
|
6949
|
+
allow: [],
|
|
6950
|
+
deny: [],
|
|
6951
|
+
allowPrivate: true,
|
|
6952
|
+
// The SSRF floor is always on and needs no default; these two only
|
|
6953
|
+
// widen or narrow it. See doc/roadmap/active/ssrf-floor-design.md.
|
|
6954
|
+
ssrfAllow: [],
|
|
6955
|
+
ssrfStrict: false
|
|
6956
|
+
},
|
|
6957
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
6958
|
+
injectionScan: { enabled: false, minConfidence: "medium", allow: [] },
|
|
6959
|
+
packageCheck: {
|
|
6960
|
+
enabled: true,
|
|
6961
|
+
onMalicious: "block",
|
|
6962
|
+
newPackage: "review",
|
|
6963
|
+
maxAgeHours: 48,
|
|
6964
|
+
installScript: "review",
|
|
6965
|
+
onlineFallback: true,
|
|
6966
|
+
allow: []
|
|
6967
|
+
},
|
|
6968
|
+
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
6969
|
+
untrustedContent: {
|
|
6970
|
+
enabled: true,
|
|
6971
|
+
spotlight: true,
|
|
6972
|
+
afterExposure: "ask-once",
|
|
6973
|
+
orgDomains: [],
|
|
6974
|
+
roles: {},
|
|
6975
|
+
trustedRecipients: []
|
|
6976
|
+
},
|
|
6977
|
+
trustedHosts: [],
|
|
6978
|
+
trustedHostsManaged: false,
|
|
6979
|
+
appPermissions: {},
|
|
6980
|
+
appLinks: {},
|
|
6981
|
+
managedJailPaths: []
|
|
6982
|
+
},
|
|
6983
|
+
environments: {}
|
|
6984
|
+
};
|
|
5924
6985
|
var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
5925
6986
|
|
|
5926
6987
|
// src/cli/render/ink/panels/cost-note.ts
|
|
@@ -6307,7 +7368,7 @@ function ShieldsPanel({ summary, width, enabledShields = [] }) {
|
|
|
6307
7368
|
});
|
|
6308
7369
|
const hitShields = ranked.filter((i) => i.totalCatches > 0);
|
|
6309
7370
|
const hitNames = new Set(hitShields.map((i) => i.shieldName));
|
|
6310
|
-
const zeroHitAll = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitNames.has(name) && !
|
|
7371
|
+
const zeroHitAll = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitNames.has(name) && !APP_SHIELD_NAMES.has(name)).sort();
|
|
6311
7372
|
const zeroHitBuiltins = zeroHitAll.filter((name) => !enabled.has(name));
|
|
6312
7373
|
const zeroHitEnabled = zeroHitAll.filter((name) => enabled.has(name));
|
|
6313
7374
|
const topRec = hitShields.find(
|