@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/index.mjs
CHANGED
|
@@ -52,15 +52,101 @@ import picomatch from "picomatch";
|
|
|
52
52
|
import safeRegex from "safe-regex2";
|
|
53
53
|
import pm from "picomatch";
|
|
54
54
|
import mvdanSh from "mvdan-sh";
|
|
55
|
-
import safeRegex2 from "safe-regex2";
|
|
56
55
|
import mvdanSh2 from "mvdan-sh";
|
|
57
|
-
import
|
|
56
|
+
import safeRegex2 from "safe-regex2";
|
|
58
57
|
import { createHash as createHash2 } from "crypto";
|
|
59
58
|
import { createHash as createHash22 } from "crypto";
|
|
60
59
|
import mvdanSh3 from "mvdan-sh";
|
|
61
60
|
import { createHash as createHash3 } from "crypto";
|
|
62
61
|
import mvdanSh4 from "mvdan-sh";
|
|
63
62
|
import crypto from "crypto";
|
|
63
|
+
function validateRegex(pattern) {
|
|
64
|
+
if (!pattern) return "Pattern is required";
|
|
65
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(pattern);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
70
|
+
}
|
|
71
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
72
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
76
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
77
|
+
const key = `${pattern}\0${flags}`;
|
|
78
|
+
if (regexCache.has(key)) {
|
|
79
|
+
const cached = regexCache.get(key);
|
|
80
|
+
regexCache.delete(key);
|
|
81
|
+
regexCache.set(key, cached);
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
if (validateRegex(pattern) !== null) return null;
|
|
85
|
+
try {
|
|
86
|
+
const re = new RegExp(pattern, flags);
|
|
87
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
88
|
+
const oldest = regexCache.keys().next().value;
|
|
89
|
+
if (oldest) regexCache.delete(oldest);
|
|
90
|
+
}
|
|
91
|
+
regexCache.set(key, re);
|
|
92
|
+
return re;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isToolRule(rule) {
|
|
98
|
+
return rule.tool.startsWith("{,mcp__*__}");
|
|
99
|
+
}
|
|
100
|
+
function actionPart(ruleName) {
|
|
101
|
+
const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
|
|
102
|
+
return m ? m[1] : ruleName ?? "rule";
|
|
103
|
+
}
|
|
104
|
+
function ownReason(reason, owner) {
|
|
105
|
+
if (!reason) return reason;
|
|
106
|
+
return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
|
|
107
|
+
}
|
|
108
|
+
function buildAppShield(spec) {
|
|
109
|
+
const owner = spec.label;
|
|
110
|
+
const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
|
|
111
|
+
...rule,
|
|
112
|
+
name: `shield:${spec.name}:${actionPart(rule.name)}`,
|
|
113
|
+
server: spec.server,
|
|
114
|
+
reason: ownReason(rule.reason, owner)
|
|
115
|
+
}));
|
|
116
|
+
const ownTools = (spec.mcpTools ?? []).map((rule) => ({
|
|
117
|
+
...rule,
|
|
118
|
+
server: spec.server
|
|
119
|
+
}));
|
|
120
|
+
return {
|
|
121
|
+
name: spec.name,
|
|
122
|
+
description: spec.description,
|
|
123
|
+
aliases: [],
|
|
124
|
+
smartRules: [...toolRules, ...ownTools, ...spec.doors],
|
|
125
|
+
dangerousWords: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildGeneralRules() {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const name of Object.keys(ACTION_TEMPLATES)) {
|
|
131
|
+
const { slug: slug2, label: label2 } = ACTION_CHECKS[name];
|
|
132
|
+
for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
|
|
133
|
+
const part = actionPart(rule.name).replace(/^review-/, "");
|
|
134
|
+
out.push({
|
|
135
|
+
...rule,
|
|
136
|
+
name: `review-action-${slug2}-${part}`,
|
|
137
|
+
notServer: APP_SERVER_GLOB,
|
|
138
|
+
reason: ownReason(rule.reason, label2)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function generalActionRuleNames(template) {
|
|
145
|
+
const { slug: slug2 } = ACTION_CHECKS[template];
|
|
146
|
+
return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
|
|
147
|
+
(r) => r.name
|
|
148
|
+
);
|
|
149
|
+
}
|
|
64
150
|
function isShieldVerdict(v) {
|
|
65
151
|
return v === "allow" || v === "review" || v === "block";
|
|
66
152
|
}
|
|
@@ -98,16 +184,25 @@ function validateOverrides(raw) {
|
|
|
98
184
|
return { overrides: result, warnings };
|
|
99
185
|
}
|
|
100
186
|
function assertBuiltinShieldRegexesAreSafe() {
|
|
101
|
-
|
|
187
|
+
const sets = [
|
|
188
|
+
...Object.values(BUILTIN_SHIELDS).map((sh) => ({
|
|
189
|
+
name: sh.name,
|
|
190
|
+
smartRules: sh.smartRules
|
|
191
|
+
})),
|
|
192
|
+
...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
|
|
193
|
+
{ name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
|
|
194
|
+
];
|
|
195
|
+
for (const shield of sets) {
|
|
102
196
|
for (const rule of shield.smartRules) {
|
|
103
197
|
const conditions = rule.conditions ?? [];
|
|
104
198
|
for (const cond of conditions) {
|
|
105
199
|
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
106
200
|
const pattern = cond.value;
|
|
107
201
|
if (!pattern) continue;
|
|
108
|
-
|
|
202
|
+
const problem = validateRegex(pattern);
|
|
203
|
+
if (problem) {
|
|
109
204
|
throw new Error(
|
|
110
|
-
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has
|
|
205
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
|
|
111
206
|
);
|
|
112
207
|
}
|
|
113
208
|
}
|
|
@@ -343,6 +438,32 @@ function ssrfFloorDecision(m, asWritten, opts) {
|
|
|
343
438
|
}
|
|
344
439
|
return "block";
|
|
345
440
|
}
|
|
441
|
+
function isModeValue(v) {
|
|
442
|
+
return typeof v === "string" && MODE_VALUES.includes(v);
|
|
443
|
+
}
|
|
444
|
+
function levelOf(mode) {
|
|
445
|
+
if (mode === "observe") return "audit";
|
|
446
|
+
return LEVELS.includes(mode ?? "") ? mode : "standard";
|
|
447
|
+
}
|
|
448
|
+
function isRecordOnly(mode) {
|
|
449
|
+
return levelOf(mode) === "audit";
|
|
450
|
+
}
|
|
451
|
+
function reviewsEveryInline(mode) {
|
|
452
|
+
const level = levelOf(mode);
|
|
453
|
+
return level === "strict" || level === "block-all";
|
|
454
|
+
}
|
|
455
|
+
function isBlockAll(mode) {
|
|
456
|
+
return levelOf(mode) === "block-all";
|
|
457
|
+
}
|
|
458
|
+
function levelCheckValue(mode, id) {
|
|
459
|
+
return LEVEL_CHECKS[levelOf(mode)][id];
|
|
460
|
+
}
|
|
461
|
+
function levelChecks(mode) {
|
|
462
|
+
return LEVEL_CHECKS[levelOf(mode)];
|
|
463
|
+
}
|
|
464
|
+
function levelPackageSignals(mode) {
|
|
465
|
+
return levelOf(mode) === "soft" ? { newPackage: "off", installScript: "off" } : void 0;
|
|
466
|
+
}
|
|
346
467
|
function isVerdict(v) {
|
|
347
468
|
return v === "off" || v === "log" || v === "review" || v === "block";
|
|
348
469
|
}
|
|
@@ -393,7 +514,7 @@ function isLockedCheck(def) {
|
|
|
393
514
|
function ssrfCheckId(tier) {
|
|
394
515
|
return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
|
|
395
516
|
}
|
|
396
|
-
function
|
|
517
|
+
function ruleNameInLabel(s) {
|
|
397
518
|
let rest = s;
|
|
398
519
|
const override = rest.lastIndexOf(OVERRIDE_PREFIX);
|
|
399
520
|
if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
|
|
@@ -409,7 +530,7 @@ function checkIdForRule(nameOrLabel) {
|
|
|
409
530
|
const s = nameOrLabel.trim();
|
|
410
531
|
const exact = LEGACY_TO_ID.get(s);
|
|
411
532
|
if (exact) return exact;
|
|
412
|
-
const unwrapped =
|
|
533
|
+
const unwrapped = ruleNameInLabel(s);
|
|
413
534
|
if (unwrapped !== void 0) return checkIdForRule(unwrapped);
|
|
414
535
|
if (s.startsWith("shield:project-jail:")) {
|
|
415
536
|
return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
|
|
@@ -488,19 +609,21 @@ function resolveCheck(settings, id) {
|
|
|
488
609
|
if (!def) return void 0;
|
|
489
610
|
if (isLockedCheck(def)) return { id, value: "block", source: "locked" };
|
|
490
611
|
const floor = def.floor ? CHECK_VERDICT_RANK[def.floor] : 0;
|
|
612
|
+
const levelDefault = levelCheckValue(settings.mode, id) ?? def.defaultValue;
|
|
613
|
+
const sourceOf = (value2) => value2 === levelDefault ? "default" : "configured";
|
|
491
614
|
const explicit = settings.checks?.[id];
|
|
492
615
|
if (isVerdict(explicit)) {
|
|
493
616
|
const value2 = CHECK_VERDICT_RANK[explicit] < floor ? def.floor : explicit;
|
|
494
|
-
return { id, value: value2, source: value2
|
|
617
|
+
return { id, value: value2, source: sourceOf(value2) };
|
|
495
618
|
}
|
|
496
619
|
if (def.pack) {
|
|
497
620
|
const on = settings.appliedShields?.includes(def.pack) ?? false;
|
|
498
621
|
return { id, value: on ? def.defaultValue : "off", source: "default" };
|
|
499
622
|
}
|
|
500
|
-
const
|
|
501
|
-
if (
|
|
502
|
-
const value = CHECK_VERDICT_RANK[
|
|
503
|
-
return { id, value, source: value
|
|
623
|
+
const read2 = LEGACY_READERS[id]?.(settings);
|
|
624
|
+
if (read2 === void 0) return { id, value: levelDefault, source: "default" };
|
|
625
|
+
const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
|
|
626
|
+
return { id, value, source: sourceOf(value) };
|
|
504
627
|
}
|
|
505
628
|
function checkValue(settings, id) {
|
|
506
629
|
return resolveCheck(settings, id)?.value ?? "off";
|
|
@@ -567,6 +690,102 @@ function scanViews(text) {
|
|
|
567
690
|
}
|
|
568
691
|
return views;
|
|
569
692
|
}
|
|
693
|
+
function isAgentInstructionPath(absPath, root) {
|
|
694
|
+
const r = root.replace(/\/$/, "");
|
|
695
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
696
|
+
const rel = absPath.slice(r.length + 1);
|
|
697
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
698
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
699
|
+
}
|
|
700
|
+
function isInside(root, p) {
|
|
701
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
702
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
703
|
+
}
|
|
704
|
+
function isPasswordHashPath(p) {
|
|
705
|
+
return PASSWORD_HASH_FILES.includes(p);
|
|
706
|
+
}
|
|
707
|
+
function isLegacySystemPath(p) {
|
|
708
|
+
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
709
|
+
}
|
|
710
|
+
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
711
|
+
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
712
|
+
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
713
|
+
const rooted = p.startsWith("/");
|
|
714
|
+
const parts = [];
|
|
715
|
+
for (const part of p.split("/")) {
|
|
716
|
+
if (!part || part === ".") continue;
|
|
717
|
+
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
718
|
+
else if (part !== ".." || !rooted) parts.push(part);
|
|
719
|
+
}
|
|
720
|
+
return (rooted ? "/" : "") + parts.join("/");
|
|
721
|
+
}
|
|
722
|
+
function inlineLanguageOf(interp) {
|
|
723
|
+
const i = interp.toLowerCase();
|
|
724
|
+
if (/^python[\d.]*$/.test(i)) return "python";
|
|
725
|
+
if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
|
|
726
|
+
if (i === "perl") return "perl";
|
|
727
|
+
if (i === "ruby" || i === "irb") return "ruby";
|
|
728
|
+
if (i === "php") return "php";
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
function resolvePythonAliases(code) {
|
|
732
|
+
const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
|
|
733
|
+
let out = code;
|
|
734
|
+
for (const [, mod, alias] of aliases) {
|
|
735
|
+
if (alias === mod) continue;
|
|
736
|
+
out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
function protectedLiteral(code, isProtected) {
|
|
741
|
+
let n = 0;
|
|
742
|
+
for (const m of code.matchAll(STRING_LITERAL)) {
|
|
743
|
+
if (++n > 2048) break;
|
|
744
|
+
const value = m[2].trim();
|
|
745
|
+
if (value && isProtected(value)) return value;
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
function amplifies(language, code) {
|
|
750
|
+
const loop = language === "python" ? /\bwhile\s+(?:True|1)\s*:/ : language === "node" ? /\bwhile\s*\(\s*(?:true|1)\s*\)|\bfor\s*\(\s*;\s*;\s*\)|\bsetInterval\s*\(/ : /\bwhile\s*\(?\s*(?:1|true)\s*\)?|\bloop\s+do\b|\bfor\s*\(\s*;\s*;\s*\)/;
|
|
751
|
+
const m = loop.exec(code);
|
|
752
|
+
if (!m) return null;
|
|
753
|
+
const body = code.slice(m.index);
|
|
754
|
+
return /\.write\s*\(|\.(?:append|write)File(?:Sync)?\s*\(|\bopen\s*\(|\bprint\s*\(\s*[^)\n]*\bfile\s*=|\.send(?:all|to)?\s*\(|\bfwrite\s*\(|\bfile_put_contents\s*\(|\bsyswrite\b/.test(
|
|
755
|
+
body
|
|
756
|
+
) ? m[0] : null;
|
|
757
|
+
}
|
|
758
|
+
function judgeInlineCode(language, code, isProtected) {
|
|
759
|
+
if (code.length > MAX_INLINE_CODE) {
|
|
760
|
+
return { outcome: "unreadable", what: "is too long for node9 to read" };
|
|
761
|
+
}
|
|
762
|
+
const text = language === "python" ? resolvePythonAliases(code) : code;
|
|
763
|
+
for (const [category, re] of TABLES[language]) {
|
|
764
|
+
const m = re.exec(text);
|
|
765
|
+
if (!m) continue;
|
|
766
|
+
const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
|
|
767
|
+
return {
|
|
768
|
+
outcome: category === "builds-code" ? "unreadable" : "dangerous",
|
|
769
|
+
what: WHAT[category],
|
|
770
|
+
evidence,
|
|
771
|
+
category
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const path28 = protectedLiteral(text, isProtected);
|
|
775
|
+
if (path28) {
|
|
776
|
+
return {
|
|
777
|
+
outcome: "dangerous",
|
|
778
|
+
what: WHAT["reads-protected"],
|
|
779
|
+
evidence: path28.slice(0, 60),
|
|
780
|
+
category: "reads-protected"
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const loop = amplifies(language, text);
|
|
784
|
+
if (loop) {
|
|
785
|
+
return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
570
789
|
function isCatHeredocOrLit(part) {
|
|
571
790
|
if (!part) return false;
|
|
572
791
|
const t = syntax.NodeType(part);
|
|
@@ -635,6 +854,55 @@ function cachedNormalize(command, compute) {
|
|
|
635
854
|
function normalizeCommandForPolicy(command) {
|
|
636
855
|
return commandReadingsImpl(command).posix;
|
|
637
856
|
}
|
|
857
|
+
function fileAccessView(command) {
|
|
858
|
+
const hit = accessViewCache.get(command);
|
|
859
|
+
if (hit !== void 0) return hit;
|
|
860
|
+
const view = fileAccessViewImpl(command);
|
|
861
|
+
if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
|
|
862
|
+
const oldest = accessViewCache.keys().next().value;
|
|
863
|
+
if (oldest !== void 0) accessViewCache.delete(oldest);
|
|
864
|
+
}
|
|
865
|
+
accessViewCache.set(command, view);
|
|
866
|
+
return view;
|
|
867
|
+
}
|
|
868
|
+
function fileAccessViewImpl(command) {
|
|
869
|
+
const text = command.replace(INTERPRETER_LINE, "");
|
|
870
|
+
const f = parseShared(text);
|
|
871
|
+
if (f === PARSE_FAIL) return text;
|
|
872
|
+
const toCharIndex = byteOffsetToCharIndex(text);
|
|
873
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
874
|
+
const edits = [];
|
|
875
|
+
try {
|
|
876
|
+
syntax.Walk(f, (node) => {
|
|
877
|
+
if (!node) return false;
|
|
878
|
+
const n = node;
|
|
879
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
880
|
+
const args = n.Args || [];
|
|
881
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
882
|
+
const head = unwrapCommandHead(words);
|
|
883
|
+
for (const i of /* @__PURE__ */ new Set([0, head])) {
|
|
884
|
+
const word = words[i];
|
|
885
|
+
if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (!PROGRAM_DIR.test(word)) continue;
|
|
889
|
+
const name = word.split(/[\\/]/).pop() ?? "";
|
|
890
|
+
if (!name) continue;
|
|
891
|
+
const s = at(args[i].Pos().Offset());
|
|
892
|
+
const e = at(args[i].End().Offset());
|
|
893
|
+
if (s < 0 || e < 0) continue;
|
|
894
|
+
edits.push([s, e, name]);
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
});
|
|
898
|
+
} catch {
|
|
899
|
+
return text;
|
|
900
|
+
}
|
|
901
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
902
|
+
let out = text;
|
|
903
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
904
|
+
return out;
|
|
905
|
+
}
|
|
638
906
|
function commandReadings(command) {
|
|
639
907
|
const r = commandReadingsImpl(command);
|
|
640
908
|
const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
|
|
@@ -1090,53 +1358,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
|
|
|
1090
1358
|
}
|
|
1091
1359
|
return i;
|
|
1092
1360
|
}
|
|
1093
|
-
function
|
|
1361
|
+
function heredocText(word) {
|
|
1362
|
+
const parts = word?.Parts ?? [];
|
|
1363
|
+
let out = "";
|
|
1364
|
+
for (const p of parts) {
|
|
1365
|
+
if (syntax.NodeType(p) !== "Lit") return null;
|
|
1366
|
+
out += p.Value ?? "";
|
|
1367
|
+
}
|
|
1368
|
+
return out;
|
|
1369
|
+
}
|
|
1370
|
+
function stdinCodeOf(redirs) {
|
|
1371
|
+
const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
|
|
1372
|
+
const herestring = deriveRedirOp("cat <<< x");
|
|
1373
|
+
for (const r of redirs) {
|
|
1374
|
+
if (!r || !redirStdinOps().has(r.Op)) continue;
|
|
1375
|
+
if (heredoc.has(r.Op)) {
|
|
1376
|
+
const text = heredocText(r.Hdoc);
|
|
1377
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1378
|
+
}
|
|
1379
|
+
if (r.Op === herestring) {
|
|
1380
|
+
const text = resolveWordLiteral(r.Word);
|
|
1381
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1382
|
+
}
|
|
1383
|
+
return { code: null, source: "file" };
|
|
1384
|
+
}
|
|
1385
|
+
return { code: null, source: "file" };
|
|
1386
|
+
}
|
|
1387
|
+
function attachedCode(interp, flag) {
|
|
1388
|
+
const eq = flag.indexOf("=");
|
|
1389
|
+
if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
|
|
1390
|
+
if (/^(pwsh|powershell)/.test(interp)) return null;
|
|
1391
|
+
const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1392
|
+
const body = flag.slice(1);
|
|
1393
|
+
const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
|
|
1394
|
+
if (at < 0) return null;
|
|
1395
|
+
let rest = body.slice(at + 1);
|
|
1396
|
+
if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
|
|
1397
|
+
return rest === "" ? null : rest;
|
|
1398
|
+
}
|
|
1399
|
+
function inlineExecCall(stmt, pipeFed) {
|
|
1094
1400
|
const cmd = stmt?.Cmd;
|
|
1095
|
-
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return
|
|
1401
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
|
|
1096
1402
|
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1097
|
-
if (words.length === 0) return
|
|
1403
|
+
if (words.length === 0) return null;
|
|
1098
1404
|
const headIdx = unwrapCommandHead(words);
|
|
1099
1405
|
const rawHead = words[headIdx];
|
|
1100
|
-
if (rawHead == null) return
|
|
1406
|
+
if (rawHead == null) return null;
|
|
1101
1407
|
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1102
|
-
if (!INLINE_INTERPRETER.test(interp)) return
|
|
1408
|
+
if (!INLINE_INTERPRETER.test(interp)) return null;
|
|
1103
1409
|
let args = words.slice(headIdx + 1);
|
|
1104
|
-
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval")
|
|
1410
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
|
|
1411
|
+
const code = args[1] ?? null;
|
|
1412
|
+
return { interp, code: [code], source: code === null ? "variable" : void 0 };
|
|
1413
|
+
}
|
|
1105
1414
|
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1106
1415
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1107
1416
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1108
1417
|
}
|
|
1418
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1419
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1420
|
+
const fromStdin = () => {
|
|
1421
|
+
if (stdinFed) {
|
|
1422
|
+
const { code, source } = stdinCodeOf(redirs);
|
|
1423
|
+
return { interp, code: [code], source };
|
|
1424
|
+
}
|
|
1425
|
+
return { interp, code: [null], source: "pipe" };
|
|
1426
|
+
};
|
|
1109
1427
|
let positionals2 = 0;
|
|
1110
1428
|
let selectedProgram = false;
|
|
1429
|
+
const codes = [];
|
|
1111
1430
|
for (let i = 0; i < args.length; i++) {
|
|
1112
1431
|
const a = args[i];
|
|
1113
1432
|
if (a == null) {
|
|
1433
|
+
if (codes.length > 0) break;
|
|
1114
1434
|
positionals2++;
|
|
1115
1435
|
selectedProgram = true;
|
|
1116
1436
|
continue;
|
|
1117
1437
|
}
|
|
1118
1438
|
if (!selectedProgram && isInlineCodeFlag(interp, a)) {
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1439
|
+
if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
|
|
1440
|
+
const payload = args[i + 1];
|
|
1441
|
+
if (payload == null) return { interp, code: [null], source: "variable" };
|
|
1442
|
+
return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
|
|
1443
|
+
}
|
|
1444
|
+
const attached = attachedCode(interp, a);
|
|
1445
|
+
if (attached !== null) {
|
|
1446
|
+
codes.push(attached);
|
|
1447
|
+
} else {
|
|
1448
|
+
codes.push(args[i + 1] ?? null);
|
|
1449
|
+
i++;
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (codes.length > 0) {
|
|
1454
|
+
if (a.startsWith("-")) continue;
|
|
1455
|
+
break;
|
|
1123
1456
|
}
|
|
1124
1457
|
if (a === "-m") {
|
|
1125
1458
|
selectedProgram = true;
|
|
1126
1459
|
continue;
|
|
1127
1460
|
}
|
|
1128
|
-
if (a === "-" && !selectedProgram) return
|
|
1461
|
+
if (a === "-" && !selectedProgram) return fromStdin();
|
|
1129
1462
|
if (!a.startsWith("-")) {
|
|
1130
1463
|
positionals2++;
|
|
1131
1464
|
selectedProgram = true;
|
|
1132
1465
|
}
|
|
1133
1466
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1467
|
+
if (codes.length > 0) {
|
|
1468
|
+
return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
|
|
1469
|
+
}
|
|
1136
1470
|
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
1137
|
-
if (!/^(bash|sh|zsh)$/i.test(interp)) return
|
|
1471
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
|
|
1138
1472
|
}
|
|
1139
|
-
return
|
|
1473
|
+
return null;
|
|
1140
1474
|
}
|
|
1141
1475
|
function shellPayloadNeedsReview(payload) {
|
|
1142
1476
|
if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
|
|
@@ -1169,7 +1503,7 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1169
1503
|
payload
|
|
1170
1504
|
))
|
|
1171
1505
|
review = true;
|
|
1172
|
-
if (!review &&
|
|
1506
|
+
if (!review && inlineExecReview(payload) !== null) review = true;
|
|
1173
1507
|
} catch {
|
|
1174
1508
|
review = true;
|
|
1175
1509
|
} finally {
|
|
@@ -1177,35 +1511,77 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1177
1511
|
}
|
|
1178
1512
|
return review;
|
|
1179
1513
|
}
|
|
1180
|
-
function
|
|
1514
|
+
function inlineExecCalls(command) {
|
|
1181
1515
|
const f = parseShared(command);
|
|
1182
|
-
if (f === PARSE_FAIL)
|
|
1183
|
-
|
|
1184
|
-
command
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
let found = false;
|
|
1516
|
+
if (f === PARSE_FAIL) return PARSE_FAIL;
|
|
1517
|
+
const calls = [];
|
|
1188
1518
|
try {
|
|
1189
1519
|
syntax.Walk(f, (node) => {
|
|
1190
|
-
if (!node ||
|
|
1520
|
+
if (!node || calls.length >= MAX_INLINE_CALLS) return false;
|
|
1191
1521
|
const n = node;
|
|
1192
1522
|
const t = syntax.NodeType(n);
|
|
1193
1523
|
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
return false;
|
|
1197
|
-
}
|
|
1524
|
+
const call = inlineExecCall(n.Y, true);
|
|
1525
|
+
if (call) calls.push(call);
|
|
1198
1526
|
}
|
|
1199
|
-
if (t === "Stmt"
|
|
1200
|
-
|
|
1201
|
-
|
|
1527
|
+
if (t === "Stmt") {
|
|
1528
|
+
const call = inlineExecCall(n, false);
|
|
1529
|
+
if (call) calls.push(call);
|
|
1202
1530
|
}
|
|
1203
1531
|
return true;
|
|
1204
1532
|
});
|
|
1205
1533
|
} catch {
|
|
1206
|
-
return found;
|
|
1207
1534
|
}
|
|
1208
|
-
return
|
|
1535
|
+
return calls;
|
|
1536
|
+
}
|
|
1537
|
+
function detectInlineExec(command) {
|
|
1538
|
+
const calls = inlineExecCalls(command);
|
|
1539
|
+
if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
|
|
1540
|
+
return calls.length > 0;
|
|
1541
|
+
}
|
|
1542
|
+
function isProtectedCodePath(p) {
|
|
1543
|
+
if (matchSensitivePath(p) !== null) return true;
|
|
1544
|
+
return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
|
|
1545
|
+
}
|
|
1546
|
+
function inlineExecDescription(r) {
|
|
1547
|
+
const lead = r ? `The AI is running code directly from the command line: ${inlineExecReason(r).replace(/^This/, "this")}.` : "The AI is running code directly from the command line.";
|
|
1548
|
+
return `${lead} Review the full script below before allowing it to execute.`;
|
|
1549
|
+
}
|
|
1550
|
+
function inlineExecReason(r) {
|
|
1551
|
+
const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
|
|
1552
|
+
return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
|
|
1553
|
+
}
|
|
1554
|
+
function inlineExecReview(command) {
|
|
1555
|
+
const calls = inlineExecCalls(command);
|
|
1556
|
+
if (calls === PARSE_FAIL) {
|
|
1557
|
+
return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
|
|
1558
|
+
}
|
|
1559
|
+
let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
|
|
1560
|
+
for (const call of calls) {
|
|
1561
|
+
const language = inlineLanguageOf(call.interp);
|
|
1562
|
+
for (const code of call.code) {
|
|
1563
|
+
let finding = null;
|
|
1564
|
+
if (code === null) {
|
|
1565
|
+
finding = {
|
|
1566
|
+
outcome: "unreadable",
|
|
1567
|
+
interp: call.interp,
|
|
1568
|
+
what: UNREADABLE_SOURCE[call.source ?? "variable"]
|
|
1569
|
+
};
|
|
1570
|
+
} else if (language === null) {
|
|
1571
|
+
finding = {
|
|
1572
|
+
outcome: "unreadable",
|
|
1573
|
+
interp: call.interp,
|
|
1574
|
+
what: `is ${call.interp} code, which node9 does not read`
|
|
1575
|
+
};
|
|
1576
|
+
} else {
|
|
1577
|
+
const judged = judgeInlineCode(language, code, isProtectedCodePath);
|
|
1578
|
+
if (judged) finding = { ...judged, interp: call.interp };
|
|
1579
|
+
}
|
|
1580
|
+
if (finding?.outcome === "dangerous") return finding;
|
|
1581
|
+
if (finding && !unreadable) unreadable = finding;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return unreadable;
|
|
1209
1585
|
}
|
|
1210
1586
|
function analyzeChmod777(command) {
|
|
1211
1587
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
@@ -2454,40 +2830,6 @@ function analyzeShellCommand(command) {
|
|
|
2454
2830
|
}
|
|
2455
2831
|
return { actions, paths, allTokens };
|
|
2456
2832
|
}
|
|
2457
|
-
function validateRegex(pattern) {
|
|
2458
|
-
if (!pattern) return "Pattern is required";
|
|
2459
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2460
|
-
try {
|
|
2461
|
-
new RegExp(pattern);
|
|
2462
|
-
} catch (e) {
|
|
2463
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2464
|
-
}
|
|
2465
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2466
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2470
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2471
|
-
const key = `${pattern}\0${flags}`;
|
|
2472
|
-
if (regexCache.has(key)) {
|
|
2473
|
-
const cached = regexCache.get(key);
|
|
2474
|
-
regexCache.delete(key);
|
|
2475
|
-
regexCache.set(key, cached);
|
|
2476
|
-
return cached;
|
|
2477
|
-
}
|
|
2478
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2479
|
-
try {
|
|
2480
|
-
const re = new RegExp(pattern, flags);
|
|
2481
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2482
|
-
const oldest = regexCache.keys().next().value;
|
|
2483
|
-
if (oldest) regexCache.delete(oldest);
|
|
2484
|
-
}
|
|
2485
|
-
regexCache.set(key, re);
|
|
2486
|
-
return re;
|
|
2487
|
-
} catch {
|
|
2488
|
-
return null;
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
2833
|
function matchesPattern(text, patterns) {
|
|
2492
2834
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2493
2835
|
if (p.length === 0) return false;
|
|
@@ -2498,9 +2840,21 @@ function matchesPattern(text, patterns) {
|
|
|
2498
2840
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2499
2841
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2500
2842
|
}
|
|
2501
|
-
function
|
|
2843
|
+
function mcpServerFromToolName(toolName) {
|
|
2844
|
+
const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
2845
|
+
return match?.[1];
|
|
2846
|
+
}
|
|
2847
|
+
function ruleServerMatches(rule, serverLabel) {
|
|
2848
|
+
const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
|
|
2849
|
+
if (rule.server) {
|
|
2850
|
+
if (!label2 || !matchesPattern(label2, rule.server)) return false;
|
|
2851
|
+
}
|
|
2852
|
+
if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2855
|
+
function getNestedValue(obj, path28) {
|
|
2502
2856
|
if (!obj || typeof obj !== "object") return null;
|
|
2503
|
-
const segments2 =
|
|
2857
|
+
const segments2 = path28.split(".");
|
|
2504
2858
|
for (const seg of segments2) {
|
|
2505
2859
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2506
2860
|
}
|
|
@@ -2520,23 +2874,26 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2520
2874
|
return val;
|
|
2521
2875
|
};
|
|
2522
2876
|
const readingsCache = /* @__PURE__ */ new Map();
|
|
2523
|
-
const resolveFieldReadings = (field) => {
|
|
2524
|
-
const
|
|
2877
|
+
const resolveFieldReadings = (field, reading) => {
|
|
2878
|
+
const access = reading === "access" && field === "command";
|
|
2879
|
+
const key = access ? `${field}\0access` : field;
|
|
2880
|
+
const cached = readingsCache.get(key);
|
|
2525
2881
|
if (cached) return cached;
|
|
2526
2882
|
const primary = resolveField(field);
|
|
2527
2883
|
if (primary === null) {
|
|
2528
|
-
readingsCache.set(
|
|
2884
|
+
readingsCache.set(key, []);
|
|
2529
2885
|
return [];
|
|
2530
2886
|
}
|
|
2531
2887
|
let out = [primary];
|
|
2532
2888
|
if (field === "command") {
|
|
2533
2889
|
const raw = getNestedValue(args, field);
|
|
2534
2890
|
if (typeof raw === "string") {
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2891
|
+
const text = access ? fileAccessView(raw) : raw;
|
|
2892
|
+
const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
|
|
2893
|
+
out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
2537
2894
|
}
|
|
2538
2895
|
}
|
|
2539
|
-
readingsCache.set(
|
|
2896
|
+
readingsCache.set(key, out);
|
|
2540
2897
|
return out;
|
|
2541
2898
|
};
|
|
2542
2899
|
const results = rule.conditions.map((cond) => {
|
|
@@ -2554,14 +2911,14 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2554
2911
|
if (val === null || !cond.value) return false;
|
|
2555
2912
|
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2556
2913
|
if (!reM) return false;
|
|
2557
|
-
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
2914
|
+
return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
|
|
2558
2915
|
}
|
|
2559
2916
|
case "notMatches": {
|
|
2560
2917
|
if (!cond.value) return false;
|
|
2561
2918
|
if (val === null) return true;
|
|
2562
2919
|
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2563
2920
|
if (!reN) return false;
|
|
2564
|
-
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
2921
|
+
return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
|
|
2565
2922
|
}
|
|
2566
2923
|
case "matchesGlob":
|
|
2567
2924
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
@@ -3403,35 +3760,6 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
3403
3760
|
}
|
|
3404
3761
|
return out;
|
|
3405
3762
|
}
|
|
3406
|
-
function isAgentInstructionPath(absPath, root) {
|
|
3407
|
-
const r = root.replace(/\/$/, "");
|
|
3408
|
-
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
3409
|
-
const rel = absPath.slice(r.length + 1);
|
|
3410
|
-
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
3411
|
-
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
3412
|
-
}
|
|
3413
|
-
function isInside(root, p) {
|
|
3414
|
-
const r = root.replace(/\/$/, "") || "/";
|
|
3415
|
-
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
3416
|
-
}
|
|
3417
|
-
function isPasswordHashPath(p) {
|
|
3418
|
-
return PASSWORD_HASH_FILES.includes(p);
|
|
3419
|
-
}
|
|
3420
|
-
function isLegacySystemPath(p) {
|
|
3421
|
-
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
3422
|
-
}
|
|
3423
|
-
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
3424
|
-
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
3425
|
-
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
3426
|
-
const rooted = p.startsWith("/");
|
|
3427
|
-
const parts = [];
|
|
3428
|
-
for (const part of p.split("/")) {
|
|
3429
|
-
if (!part || part === ".") continue;
|
|
3430
|
-
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
3431
|
-
else if (part !== ".." || !rooted) parts.push(part);
|
|
3432
|
-
}
|
|
3433
|
-
return (rooted ? "/" : "") + parts.join("/");
|
|
3434
|
-
}
|
|
3435
3763
|
function incompleteFileVerdict(operation = "write") {
|
|
3436
3764
|
return {
|
|
3437
3765
|
decision: "review",
|
|
@@ -3483,8 +3811,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
3483
3811
|
}
|
|
3484
3812
|
return out;
|
|
3485
3813
|
}
|
|
3486
|
-
function within(
|
|
3487
|
-
return
|
|
3814
|
+
function within(path28, root) {
|
|
3815
|
+
return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
|
|
3488
3816
|
}
|
|
3489
3817
|
function hits(access, paths, directories, context) {
|
|
3490
3818
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -4292,14 +4620,14 @@ function sensitivePathMatch(originalPath) {
|
|
|
4292
4620
|
}
|
|
4293
4621
|
function assertBuiltinPatternsAreSafe() {
|
|
4294
4622
|
for (const p of DLP_PATTERNS) {
|
|
4295
|
-
if (!
|
|
4623
|
+
if (!safeRegex2(p.regex.source)) {
|
|
4296
4624
|
throw new Error(
|
|
4297
4625
|
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
4298
4626
|
);
|
|
4299
4627
|
}
|
|
4300
4628
|
}
|
|
4301
4629
|
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
4302
|
-
if (!
|
|
4630
|
+
if (!safeRegex2(re.source)) {
|
|
4303
4631
|
throw new Error(
|
|
4304
4632
|
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
4305
4633
|
);
|
|
@@ -5045,6 +5373,20 @@ function extractPackageInstalls(command) {
|
|
|
5045
5373
|
return true;
|
|
5046
5374
|
});
|
|
5047
5375
|
}
|
|
5376
|
+
function tuningFromLegacyPolicy(policy) {
|
|
5377
|
+
const out = {};
|
|
5378
|
+
const p = policy;
|
|
5379
|
+
if (policy.packageCheck?.registrySignals === false) {
|
|
5380
|
+
out["loading.malicious-package"] = { newPackage: "off", installScript: "off" };
|
|
5381
|
+
}
|
|
5382
|
+
for (const f of TUNING_FIELDS) {
|
|
5383
|
+
const [block, key] = f.legacy;
|
|
5384
|
+
const value = p[block]?.[key];
|
|
5385
|
+
if (value === void 0) continue;
|
|
5386
|
+
(out[f.checkId] ??= {})[f.name] = value;
|
|
5387
|
+
}
|
|
5388
|
+
return out;
|
|
5389
|
+
}
|
|
5048
5390
|
function tuningToLegacyPolicy(policy, tuning) {
|
|
5049
5391
|
const unknown = [];
|
|
5050
5392
|
for (const [id, fields] of Object.entries(tuning)) {
|
|
@@ -5159,6 +5501,25 @@ function checkToLegacyPolicy(policy, id, v) {
|
|
|
5159
5501
|
return false;
|
|
5160
5502
|
}
|
|
5161
5503
|
}
|
|
5504
|
+
function knobStatedChecks(policy) {
|
|
5505
|
+
const out = /* @__PURE__ */ new Set();
|
|
5506
|
+
const has = (o, key) => !!o && typeof o === "object" && o[key] !== void 0;
|
|
5507
|
+
const { dlp, egress, loopDetection, injectionScan, skillPinning, packageCheck } = policy;
|
|
5508
|
+
const uc = policy.untrustedContent;
|
|
5509
|
+
if (has(dlp, "enabled")) out.add("data.secrets");
|
|
5510
|
+
if (has(dlp, "reviewAction") || dlp?.enabled === false) out.add("data.secrets-weak");
|
|
5511
|
+
if (has(dlp, "pii")) out.add("data.pii");
|
|
5512
|
+
if (has(egress, "enabled") || has(egress, "mode")) out.add("network.unknown-host");
|
|
5513
|
+
if (has(egress, "ssrfStrict")) out.add("network.internal-addresses");
|
|
5514
|
+
if (has(loopDetection, "enabled")) out.add("behavior.loops");
|
|
5515
|
+
if (has(injectionScan, "enabled")) out.add("behavior.prompt-injection");
|
|
5516
|
+
if (has(skillPinning, "enabled") || has(skillPinning, "mode")) out.add("loading.skill-tamper");
|
|
5517
|
+
if (has(packageCheck, "enabled") || has(packageCheck, "onMalicious"))
|
|
5518
|
+
out.add("loading.malicious-package");
|
|
5519
|
+
if (uc && typeof uc === "object" && (uc.enabled === false || has(uc, "afterExposure")))
|
|
5520
|
+
out.add("behavior.untrusted-content");
|
|
5521
|
+
return out;
|
|
5522
|
+
}
|
|
5162
5523
|
function isKnobGoverned(id) {
|
|
5163
5524
|
return id in KNOB_GOVERNED;
|
|
5164
5525
|
}
|
|
@@ -5423,9 +5784,9 @@ function bareToolName(toolName) {
|
|
|
5423
5784
|
const parts = toolName.split("__");
|
|
5424
5785
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
5425
5786
|
}
|
|
5426
|
-
function valuesAt(args,
|
|
5787
|
+
function valuesAt(args, path28) {
|
|
5427
5788
|
let cursors = [args];
|
|
5428
|
-
for (const rawSegment of
|
|
5789
|
+
for (const rawSegment of path28.split(".")) {
|
|
5429
5790
|
const isArray = rawSegment.endsWith("[]");
|
|
5430
5791
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
5431
5792
|
const next = [];
|
|
@@ -5456,8 +5817,8 @@ function extractToolDestinations(toolName, args) {
|
|
|
5456
5817
|
try {
|
|
5457
5818
|
const out = [];
|
|
5458
5819
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
|
|
5459
|
-
for (const
|
|
5460
|
-
for (const value of valuesAt(args,
|
|
5820
|
+
for (const path28 of paths) {
|
|
5821
|
+
for (const value of valuesAt(args, path28)) {
|
|
5461
5822
|
const host2 = hostOf(value);
|
|
5462
5823
|
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
5463
5824
|
}
|
|
@@ -5483,8 +5844,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5483
5844
|
try {
|
|
5484
5845
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
5485
5846
|
if (!paths) return null;
|
|
5486
|
-
for (const
|
|
5487
|
-
for (const value of valuesAt(args,
|
|
5847
|
+
for (const path28 of paths) {
|
|
5848
|
+
for (const value of valuesAt(args, path28)) {
|
|
5488
5849
|
const host2 = hostOf(value);
|
|
5489
5850
|
if (!host2) continue;
|
|
5490
5851
|
const m = classifySsrf(host2);
|
|
@@ -5493,7 +5854,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5493
5854
|
const decision = ssrfFloorDecision(m, written, opts);
|
|
5494
5855
|
if (decision === null) continue;
|
|
5495
5856
|
const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
|
|
5496
|
-
return { ...m, argPath:
|
|
5857
|
+
return { ...m, argPath: path28, host: host2, reason, decision };
|
|
5497
5858
|
}
|
|
5498
5859
|
}
|
|
5499
5860
|
return null;
|
|
@@ -5873,20 +6234,26 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5873
6234
|
};
|
|
5874
6235
|
const ruleCheck = (rule) => {
|
|
5875
6236
|
if (rule.verdict === "allow") return void 0;
|
|
5876
|
-
if (!rule.builtin
|
|
6237
|
+
if (!rule.builtin) return void 0;
|
|
5877
6238
|
const id = checkIdForRule(rule.name);
|
|
5878
6239
|
if (!id) return void 0;
|
|
5879
6240
|
const def = getCheck(id);
|
|
5880
6241
|
if (!def) return void 0;
|
|
5881
|
-
if (rule.
|
|
6242
|
+
if (rule.pinned) {
|
|
6243
|
+
const explicit = config.policy.checks?.[id];
|
|
6244
|
+
return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
|
|
6245
|
+
}
|
|
6246
|
+
if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
|
|
5882
6247
|
if (def.pack) {
|
|
5883
6248
|
const explicit = config.policy.checks?.[id];
|
|
5884
6249
|
return isVerdict(explicit) ? { id, value: explicit } : void 0;
|
|
5885
6250
|
}
|
|
5886
6251
|
return { id, value: cv(id) };
|
|
5887
6252
|
};
|
|
6253
|
+
const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
|
|
5888
6254
|
const matches = config.policy.smartRules.filter((rule) => {
|
|
5889
6255
|
if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
|
|
6256
|
+
if (!ruleServerMatches(rule, serverLabel)) return false;
|
|
5890
6257
|
if (astSuppressed(rule)) return false;
|
|
5891
6258
|
if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
|
|
5892
6259
|
if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
|
|
@@ -5963,11 +6330,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5963
6330
|
if (v?.logged) noteLogged(v);
|
|
5964
6331
|
else if (v) candidates.push(v);
|
|
5965
6332
|
};
|
|
5966
|
-
|
|
6333
|
+
const inline = reviewsEveryInline(config.settings.mode) ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
|
|
6334
|
+
if (inline !== null) {
|
|
5967
6335
|
consider("commands.inline-exec", {
|
|
5968
6336
|
decision: "review",
|
|
5969
6337
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
5970
|
-
|
|
6338
|
+
...inline ? { reason: inlineExecReason(inline) } : {},
|
|
6339
|
+
ruleDescription: inlineExecDescription(inline),
|
|
5971
6340
|
tier: 3,
|
|
5972
6341
|
checkId: "commands.inline-exec"
|
|
5973
6342
|
});
|
|
@@ -6021,21 +6390,17 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
6021
6390
|
if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
|
|
6022
6391
|
const prov = checkProvenance2(firstToken, cwd);
|
|
6023
6392
|
if (prov.trustLevel === "suspect") {
|
|
6024
|
-
const
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
{
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
tier: 3,
|
|
6032
|
-
checkId: "commands.temp-binary"
|
|
6033
|
-
}
|
|
6034
|
-
);
|
|
6393
|
+
const v = applyCheckValue(cv("commands.temp-binary"), {
|
|
6394
|
+
decision: "review",
|
|
6395
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
6396
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
6397
|
+
tier: 3,
|
|
6398
|
+
checkId: "commands.temp-binary"
|
|
6399
|
+
});
|
|
6035
6400
|
if (v?.logged) noteLogged(v);
|
|
6036
6401
|
else if (v) return v;
|
|
6037
6402
|
}
|
|
6038
|
-
if (prov.trustLevel === "unknown" && config.settings.mode
|
|
6403
|
+
if (prov.trustLevel === "unknown" && isBlockAll(config.settings.mode)) {
|
|
6039
6404
|
return {
|
|
6040
6405
|
decision: "review",
|
|
6041
6406
|
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
@@ -6126,7 +6491,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
6126
6491
|
if (v?.logged) noteLogged(v);
|
|
6127
6492
|
else if (v) return v;
|
|
6128
6493
|
}
|
|
6129
|
-
if (config.settings.mode
|
|
6494
|
+
if (isBlockAll(config.settings.mode)) {
|
|
6130
6495
|
if (activeEnvironment?.requireApproval === false) return allowed();
|
|
6131
6496
|
return {
|
|
6132
6497
|
decision: "review",
|
|
@@ -7057,10 +7422,13 @@ function matchCanaryArgs(args, values) {
|
|
|
7057
7422
|
return null;
|
|
7058
7423
|
}
|
|
7059
7424
|
}
|
|
7060
|
-
var aws_default, bash_safe_default, docker_default, filesystem_default,
|
|
7425
|
+
var MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, finance_default, identity_default, publishing_default, ACTION_TEMPLATES, ACTION_CHECKS, GAP, prog, httpTo, httpWrite, HTTP_WRITE, HTTP_WRITE_ANY, NOT_STRIPE_TEST, postTo, STRIPE_OBJECTS, APP_SHIELD_SPECS, APP_SHIELDS, APP_SHIELD_NAMES, APP_SERVER_GLOB, GENERAL_ACTION_RULES, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, LEVELS, MODE_VALUES, SOFT, STRICT, BLOCK_ALL, LEVEL_CHECKS, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, WHAT, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, TABLES, STRING_LITERAL, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, INTERPRETER_LINE, PROGRAM_DIR, ACCESS_VIEW_CACHE_MAX, accessViewCache, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, MAX_INLINE_CALLS, INLINE_EXEC_FALLBACK, UNREADABLE_SOURCE, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
7061
7426
|
var init_dist = __esm({
|
|
7062
7427
|
"packages/policy-engine/dist/index.mjs"() {
|
|
7063
7428
|
"use strict";
|
|
7429
|
+
MAX_REGEX_LENGTH = 256;
|
|
7430
|
+
REGEX_CACHE_MAX = 500;
|
|
7431
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
7064
7432
|
aws_default = {
|
|
7065
7433
|
name: "aws",
|
|
7066
7434
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -7362,77 +7730,6 @@ var init_dist = __esm({
|
|
|
7362
7730
|
],
|
|
7363
7731
|
dangerousWords: ["wipefs"]
|
|
7364
7732
|
};
|
|
7365
|
-
finance_default = {
|
|
7366
|
-
name: "finance",
|
|
7367
|
-
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.",
|
|
7368
|
-
aliases: ["payments", "money"],
|
|
7369
|
-
smartRules: [
|
|
7370
|
-
{
|
|
7371
|
-
name: "shield:finance:review-refund",
|
|
7372
|
-
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
7373
|
-
conditions: [],
|
|
7374
|
-
verdict: "review",
|
|
7375
|
-
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
7376
|
-
},
|
|
7377
|
-
{
|
|
7378
|
-
name: "shield:finance:review-payout",
|
|
7379
|
-
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
7380
|
-
conditions: [],
|
|
7381
|
-
verdict: "review",
|
|
7382
|
-
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
7383
|
-
},
|
|
7384
|
-
{
|
|
7385
|
-
name: "shield:finance:review-transfer",
|
|
7386
|
-
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}",
|
|
7387
|
-
conditions: [],
|
|
7388
|
-
verdict: "review",
|
|
7389
|
-
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
7390
|
-
},
|
|
7391
|
-
{
|
|
7392
|
-
name: "shield:finance:review-charge",
|
|
7393
|
-
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}",
|
|
7394
|
-
conditions: [],
|
|
7395
|
-
verdict: "review",
|
|
7396
|
-
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
7397
|
-
},
|
|
7398
|
-
{
|
|
7399
|
-
name: "shield:finance:review-payment-link",
|
|
7400
|
-
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
7401
|
-
conditions: [],
|
|
7402
|
-
verdict: "review",
|
|
7403
|
-
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
7404
|
-
},
|
|
7405
|
-
{
|
|
7406
|
-
name: "shield:finance:review-subscription",
|
|
7407
|
-
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
7408
|
-
conditions: [],
|
|
7409
|
-
verdict: "review",
|
|
7410
|
-
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
7411
|
-
},
|
|
7412
|
-
{
|
|
7413
|
-
name: "shield:finance:review-dispute",
|
|
7414
|
-
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
7415
|
-
conditions: [],
|
|
7416
|
-
verdict: "review",
|
|
7417
|
-
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
7418
|
-
},
|
|
7419
|
-
{
|
|
7420
|
-
name: "shield:finance:review-card",
|
|
7421
|
-
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
7422
|
-
conditions: [],
|
|
7423
|
-
verdict: "review",
|
|
7424
|
-
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
7425
|
-
},
|
|
7426
|
-
{
|
|
7427
|
-
name: "shield:finance:review-withdraw",
|
|
7428
|
-
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
7429
|
-
conditions: [],
|
|
7430
|
-
verdict: "review",
|
|
7431
|
-
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
7432
|
-
}
|
|
7433
|
-
],
|
|
7434
|
-
dangerousWords: []
|
|
7435
|
-
};
|
|
7436
7733
|
github_default = {
|
|
7437
7734
|
name: "github",
|
|
7438
7735
|
description: "Protects GitHub repositories from destructive AI operations",
|
|
@@ -7459,56 +7756,6 @@ var init_dist = __esm({
|
|
|
7459
7756
|
],
|
|
7460
7757
|
dangerousWords: []
|
|
7461
7758
|
};
|
|
7462
|
-
identity_default = {
|
|
7463
|
-
name: "identity",
|
|
7464
|
-
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.",
|
|
7465
|
-
aliases: ["access", "iam"],
|
|
7466
|
-
smartRules: [
|
|
7467
|
-
{
|
|
7468
|
-
name: "shield:identity:review-password",
|
|
7469
|
-
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
7470
|
-
conditions: [],
|
|
7471
|
-
verdict: "review",
|
|
7472
|
-
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
7473
|
-
},
|
|
7474
|
-
{
|
|
7475
|
-
name: "shield:identity:review-key",
|
|
7476
|
-
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}",
|
|
7477
|
-
conditions: [],
|
|
7478
|
-
verdict: "review",
|
|
7479
|
-
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
7480
|
-
},
|
|
7481
|
-
{
|
|
7482
|
-
name: "shield:identity:review-access",
|
|
7483
|
-
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}",
|
|
7484
|
-
conditions: [],
|
|
7485
|
-
verdict: "review",
|
|
7486
|
-
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
7487
|
-
},
|
|
7488
|
-
{
|
|
7489
|
-
name: "shield:identity:review-membership",
|
|
7490
|
-
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}",
|
|
7491
|
-
conditions: [],
|
|
7492
|
-
verdict: "review",
|
|
7493
|
-
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
7494
|
-
},
|
|
7495
|
-
{
|
|
7496
|
-
name: "shield:identity:review-user",
|
|
7497
|
-
tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
7498
|
-
conditions: [],
|
|
7499
|
-
verdict: "review",
|
|
7500
|
-
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
7501
|
-
},
|
|
7502
|
-
{
|
|
7503
|
-
name: "shield:identity:review-mfa",
|
|
7504
|
-
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
7505
|
-
conditions: [],
|
|
7506
|
-
verdict: "review",
|
|
7507
|
-
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
7508
|
-
}
|
|
7509
|
-
],
|
|
7510
|
-
dangerousWords: []
|
|
7511
|
-
};
|
|
7512
7759
|
k8s_default = {
|
|
7513
7760
|
name: "k8s",
|
|
7514
7761
|
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
@@ -7761,7 +8008,7 @@ var init_dist = __esm({
|
|
|
7761
8008
|
{
|
|
7762
8009
|
field: "command",
|
|
7763
8010
|
op: "matches",
|
|
7764
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
8011
|
+
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;&|>)<]|$)",
|
|
7765
8012
|
flags: "i"
|
|
7766
8013
|
}
|
|
7767
8014
|
],
|
|
@@ -7775,7 +8022,7 @@ var init_dist = __esm({
|
|
|
7775
8022
|
{
|
|
7776
8023
|
field: "command",
|
|
7777
8024
|
op: "matches",
|
|
7778
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
8025
|
+
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)",
|
|
7779
8026
|
flags: "i"
|
|
7780
8027
|
}
|
|
7781
8028
|
],
|
|
@@ -7841,272 +8088,879 @@ var init_dist = __esm({
|
|
|
7841
8088
|
],
|
|
7842
8089
|
dangerousWords: []
|
|
7843
8090
|
};
|
|
7844
|
-
|
|
7845
|
-
name: "
|
|
7846
|
-
description: "
|
|
7847
|
-
aliases: [
|
|
8091
|
+
redis_default = {
|
|
8092
|
+
name: "redis",
|
|
8093
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
8094
|
+
aliases: [],
|
|
8095
|
+
_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.",
|
|
7848
8096
|
smartRules: [
|
|
7849
8097
|
{
|
|
7850
|
-
name: "shield:
|
|
7851
|
-
tool: "
|
|
7852
|
-
conditions: [],
|
|
7853
|
-
verdict: "review",
|
|
7854
|
-
reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
|
|
7855
|
-
},
|
|
7856
|
-
{
|
|
7857
|
-
name: "shield:publishing:review-package",
|
|
7858
|
-
tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
|
|
7859
|
-
conditions: [],
|
|
7860
|
-
verdict: "review",
|
|
7861
|
-
reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
|
|
7862
|
-
},
|
|
7863
|
-
{
|
|
7864
|
-
name: "shield:publishing:review-deploy",
|
|
7865
|
-
tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
|
|
7866
|
-
conditions: [],
|
|
7867
|
-
verdict: "review",
|
|
7868
|
-
reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
|
|
7869
|
-
},
|
|
7870
|
-
{
|
|
7871
|
-
name: "shield:publishing:review-post",
|
|
7872
|
-
tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
|
|
7873
|
-
conditions: [],
|
|
7874
|
-
verdict: "review",
|
|
7875
|
-
reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
|
|
7876
|
-
},
|
|
7877
|
-
{
|
|
7878
|
-
name: "shield:publishing:review-visibility",
|
|
7879
|
-
tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
|
|
7880
|
-
conditions: [],
|
|
7881
|
-
verdict: "review",
|
|
7882
|
-
reason: "This makes something public. node9 asks before publishing (Publishing shield)."
|
|
7883
|
-
},
|
|
7884
|
-
{
|
|
7885
|
-
name: "shield:publishing:review-npm-publish",
|
|
7886
|
-
tool: "bash",
|
|
8098
|
+
name: "shield:redis:block-flushall",
|
|
8099
|
+
tool: "*",
|
|
7887
8100
|
conditions: [
|
|
7888
8101
|
{
|
|
7889
8102
|
field: "command",
|
|
7890
8103
|
op: "matches",
|
|
7891
|
-
value: "(
|
|
8104
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
7892
8105
|
flags: "i"
|
|
7893
8106
|
}
|
|
7894
8107
|
],
|
|
7895
|
-
verdict: "
|
|
7896
|
-
reason: "
|
|
8108
|
+
verdict: "block",
|
|
8109
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
7897
8110
|
},
|
|
7898
8111
|
{
|
|
7899
|
-
name: "shield:
|
|
7900
|
-
tool: "
|
|
8112
|
+
name: "shield:redis:block-flushdb",
|
|
8113
|
+
tool: "*",
|
|
7901
8114
|
conditions: [
|
|
7902
8115
|
{
|
|
7903
8116
|
field: "command",
|
|
7904
8117
|
op: "matches",
|
|
7905
|
-
value: "(
|
|
8118
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
7906
8119
|
flags: "i"
|
|
7907
8120
|
}
|
|
7908
8121
|
],
|
|
7909
|
-
verdict: "
|
|
7910
|
-
reason: "
|
|
8122
|
+
verdict: "block",
|
|
8123
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
7911
8124
|
},
|
|
7912
8125
|
{
|
|
7913
|
-
name: "shield:
|
|
7914
|
-
tool: "
|
|
8126
|
+
name: "shield:redis:block-config-resetstat",
|
|
8127
|
+
tool: "*",
|
|
7915
8128
|
conditions: [
|
|
7916
8129
|
{
|
|
7917
8130
|
field: "command",
|
|
7918
8131
|
op: "matches",
|
|
7919
|
-
value: "(
|
|
8132
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
7920
8133
|
flags: "i"
|
|
7921
8134
|
}
|
|
7922
8135
|
],
|
|
7923
|
-
verdict: "
|
|
7924
|
-
reason: "
|
|
8136
|
+
verdict: "block",
|
|
8137
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
7925
8138
|
},
|
|
7926
8139
|
{
|
|
7927
|
-
name: "shield:
|
|
7928
|
-
tool: "
|
|
8140
|
+
name: "shield:redis:review-config-set",
|
|
8141
|
+
tool: "*",
|
|
7929
8142
|
conditions: [
|
|
7930
8143
|
{
|
|
7931
8144
|
field: "command",
|
|
7932
8145
|
op: "matches",
|
|
7933
|
-
value: "(
|
|
8146
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
7934
8147
|
flags: "i"
|
|
7935
8148
|
}
|
|
7936
8149
|
],
|
|
7937
8150
|
verdict: "review",
|
|
7938
|
-
reason: "
|
|
8151
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
7939
8152
|
},
|
|
7940
8153
|
{
|
|
7941
|
-
name: "shield:
|
|
7942
|
-
tool: "
|
|
8154
|
+
name: "shield:redis:review-del-wildcard",
|
|
8155
|
+
tool: "*",
|
|
7943
8156
|
conditions: [
|
|
7944
8157
|
{
|
|
7945
8158
|
field: "command",
|
|
7946
8159
|
op: "matches",
|
|
7947
|
-
value: "(
|
|
8160
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
7948
8161
|
flags: "i"
|
|
7949
8162
|
}
|
|
7950
8163
|
],
|
|
7951
8164
|
verdict: "review",
|
|
7952
|
-
reason: "
|
|
8165
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
8166
|
+
}
|
|
8167
|
+
],
|
|
8168
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
8169
|
+
};
|
|
8170
|
+
finance_default = {
|
|
8171
|
+
name: "finance",
|
|
8172
|
+
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.",
|
|
8173
|
+
aliases: ["payments", "money"],
|
|
8174
|
+
smartRules: [
|
|
8175
|
+
{
|
|
8176
|
+
name: "shield:finance:review-refund",
|
|
8177
|
+
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
8178
|
+
conditions: [],
|
|
8179
|
+
verdict: "review",
|
|
8180
|
+
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
7953
8181
|
},
|
|
7954
8182
|
{
|
|
7955
|
-
name: "shield:
|
|
7956
|
-
tool: "
|
|
7957
|
-
conditions: [
|
|
7958
|
-
{
|
|
7959
|
-
field: "command",
|
|
7960
|
-
op: "matches",
|
|
7961
|
-
value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
7962
|
-
flags: "i"
|
|
7963
|
-
}
|
|
7964
|
-
],
|
|
8183
|
+
name: "shield:finance:review-payout",
|
|
8184
|
+
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
8185
|
+
conditions: [],
|
|
7965
8186
|
verdict: "review",
|
|
7966
|
-
reason: "
|
|
8187
|
+
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
7967
8188
|
},
|
|
7968
8189
|
{
|
|
7969
|
-
name: "shield:
|
|
7970
|
-
tool: "
|
|
7971
|
-
conditions: [
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
8190
|
+
name: "shield:finance:review-transfer",
|
|
8191
|
+
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
|
|
8192
|
+
conditions: [],
|
|
8193
|
+
verdict: "review",
|
|
8194
|
+
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
8195
|
+
},
|
|
8196
|
+
{
|
|
8197
|
+
name: "shield:finance:review-charge",
|
|
8198
|
+
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}",
|
|
8199
|
+
conditions: [],
|
|
8200
|
+
verdict: "review",
|
|
8201
|
+
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
8202
|
+
},
|
|
8203
|
+
{
|
|
8204
|
+
name: "shield:finance:review-payment-link",
|
|
8205
|
+
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
8206
|
+
conditions: [],
|
|
8207
|
+
verdict: "review",
|
|
8208
|
+
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
8209
|
+
},
|
|
8210
|
+
{
|
|
8211
|
+
name: "shield:finance:review-subscription",
|
|
8212
|
+
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
8213
|
+
conditions: [],
|
|
8214
|
+
verdict: "review",
|
|
8215
|
+
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
8216
|
+
},
|
|
8217
|
+
{
|
|
8218
|
+
name: "shield:finance:review-dispute",
|
|
8219
|
+
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
8220
|
+
conditions: [],
|
|
8221
|
+
verdict: "review",
|
|
8222
|
+
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
8223
|
+
},
|
|
8224
|
+
{
|
|
8225
|
+
name: "shield:finance:review-card",
|
|
8226
|
+
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
8227
|
+
conditions: [],
|
|
8228
|
+
verdict: "review",
|
|
8229
|
+
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
8230
|
+
},
|
|
8231
|
+
{
|
|
8232
|
+
name: "shield:finance:review-withdraw",
|
|
8233
|
+
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
8234
|
+
conditions: [],
|
|
8235
|
+
verdict: "review",
|
|
8236
|
+
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
8237
|
+
}
|
|
8238
|
+
],
|
|
8239
|
+
dangerousWords: []
|
|
8240
|
+
};
|
|
8241
|
+
identity_default = {
|
|
8242
|
+
name: "identity",
|
|
8243
|
+
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.",
|
|
8244
|
+
aliases: ["access", "iam"],
|
|
8245
|
+
smartRules: [
|
|
8246
|
+
{
|
|
8247
|
+
name: "shield:identity:review-password",
|
|
8248
|
+
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
8249
|
+
conditions: [],
|
|
8250
|
+
verdict: "review",
|
|
8251
|
+
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
8252
|
+
},
|
|
8253
|
+
{
|
|
8254
|
+
name: "shield:identity:review-key",
|
|
8255
|
+
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}",
|
|
8256
|
+
conditions: [],
|
|
8257
|
+
verdict: "review",
|
|
8258
|
+
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
8259
|
+
},
|
|
8260
|
+
{
|
|
8261
|
+
name: "shield:identity:review-access",
|
|
8262
|
+
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}",
|
|
8263
|
+
conditions: [],
|
|
8264
|
+
verdict: "review",
|
|
8265
|
+
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
8266
|
+
},
|
|
8267
|
+
{
|
|
8268
|
+
name: "shield:identity:review-membership",
|
|
8269
|
+
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}",
|
|
8270
|
+
conditions: [],
|
|
8271
|
+
verdict: "review",
|
|
8272
|
+
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
8273
|
+
},
|
|
8274
|
+
{
|
|
8275
|
+
name: "shield:identity:review-user",
|
|
8276
|
+
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}",
|
|
8277
|
+
conditions: [],
|
|
8278
|
+
verdict: "review",
|
|
8279
|
+
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
8280
|
+
},
|
|
8281
|
+
{
|
|
8282
|
+
name: "shield:identity:review-mfa",
|
|
8283
|
+
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
8284
|
+
conditions: [],
|
|
8285
|
+
verdict: "review",
|
|
8286
|
+
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
8287
|
+
}
|
|
8288
|
+
],
|
|
8289
|
+
dangerousWords: []
|
|
8290
|
+
};
|
|
8291
|
+
publishing_default = {
|
|
8292
|
+
name: "publishing",
|
|
8293
|
+
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.",
|
|
8294
|
+
aliases: ["release", "deploy"],
|
|
8295
|
+
smartRules: [
|
|
8296
|
+
{
|
|
8297
|
+
name: "shield:publishing:review-release",
|
|
8298
|
+
tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
|
|
8299
|
+
conditions: [],
|
|
8300
|
+
verdict: "review",
|
|
8301
|
+
reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
|
|
8302
|
+
},
|
|
8303
|
+
{
|
|
8304
|
+
name: "shield:publishing:review-package",
|
|
8305
|
+
tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
|
|
8306
|
+
conditions: [],
|
|
8307
|
+
verdict: "review",
|
|
8308
|
+
reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
|
|
8309
|
+
},
|
|
8310
|
+
{
|
|
8311
|
+
name: "shield:publishing:review-deploy",
|
|
8312
|
+
tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
|
|
8313
|
+
conditions: [],
|
|
8314
|
+
verdict: "review",
|
|
8315
|
+
reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
|
|
8316
|
+
},
|
|
8317
|
+
{
|
|
8318
|
+
name: "shield:publishing:review-post",
|
|
8319
|
+
tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
|
|
8320
|
+
conditions: [],
|
|
8321
|
+
verdict: "review",
|
|
8322
|
+
reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
|
|
8323
|
+
},
|
|
8324
|
+
{
|
|
8325
|
+
name: "shield:publishing:review-visibility",
|
|
8326
|
+
tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
|
|
8327
|
+
conditions: [],
|
|
8328
|
+
verdict: "review",
|
|
8329
|
+
reason: "This makes something public. node9 asks before publishing (Publishing shield)."
|
|
8330
|
+
},
|
|
8331
|
+
{
|
|
8332
|
+
name: "shield:publishing:review-npm-publish",
|
|
8333
|
+
tool: "bash",
|
|
8334
|
+
conditions: [
|
|
8335
|
+
{
|
|
8336
|
+
field: "command",
|
|
8337
|
+
op: "matches",
|
|
8338
|
+
value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
7976
8339
|
flags: "i"
|
|
7977
8340
|
}
|
|
7978
8341
|
],
|
|
7979
8342
|
verdict: "review",
|
|
7980
|
-
reason: "
|
|
8343
|
+
reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
|
|
7981
8344
|
},
|
|
7982
8345
|
{
|
|
7983
|
-
name: "shield:publishing:review-
|
|
8346
|
+
name: "shield:publishing:review-yarn-publish",
|
|
7984
8347
|
tool: "bash",
|
|
7985
8348
|
conditions: [
|
|
7986
8349
|
{
|
|
7987
8350
|
field: "command",
|
|
7988
8351
|
op: "matches",
|
|
7989
|
-
value: "(?:^|[^\\w-])(?:
|
|
8352
|
+
value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
7990
8353
|
flags: "i"
|
|
7991
8354
|
}
|
|
7992
8355
|
],
|
|
7993
8356
|
verdict: "review",
|
|
7994
|
-
reason: "
|
|
8357
|
+
reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
|
|
7995
8358
|
},
|
|
7996
8359
|
{
|
|
7997
|
-
name: "shield:publishing:review-
|
|
8360
|
+
name: "shield:publishing:review-twine-upload",
|
|
7998
8361
|
tool: "bash",
|
|
7999
8362
|
conditions: [
|
|
8000
8363
|
{
|
|
8001
8364
|
field: "command",
|
|
8002
8365
|
op: "matches",
|
|
8003
|
-
value: "(?:^|[^\\w-])(?:
|
|
8366
|
+
value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
|
|
8004
8367
|
flags: "i"
|
|
8005
8368
|
}
|
|
8006
8369
|
],
|
|
8007
8370
|
verdict: "review",
|
|
8008
|
-
reason: "
|
|
8009
|
-
}
|
|
8010
|
-
],
|
|
8011
|
-
dangerousWords: []
|
|
8012
|
-
};
|
|
8013
|
-
redis_default = {
|
|
8014
|
-
name: "redis",
|
|
8015
|
-
description: "Protects Redis instances from destructive AI operations",
|
|
8016
|
-
aliases: [],
|
|
8017
|
-
_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.",
|
|
8018
|
-
smartRules: [
|
|
8371
|
+
reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
|
|
8372
|
+
},
|
|
8019
8373
|
{
|
|
8020
|
-
name: "shield:
|
|
8021
|
-
tool: "
|
|
8374
|
+
name: "shield:publishing:review-python-publish",
|
|
8375
|
+
tool: "bash",
|
|
8022
8376
|
conditions: [
|
|
8023
8377
|
{
|
|
8024
8378
|
field: "command",
|
|
8025
8379
|
op: "matches",
|
|
8026
|
-
value: "(
|
|
8380
|
+
value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
8027
8381
|
flags: "i"
|
|
8028
8382
|
}
|
|
8029
8383
|
],
|
|
8030
|
-
verdict: "
|
|
8031
|
-
reason: "
|
|
8384
|
+
verdict: "review",
|
|
8385
|
+
reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
|
|
8032
8386
|
},
|
|
8033
8387
|
{
|
|
8034
|
-
name: "shield:
|
|
8035
|
-
tool: "
|
|
8388
|
+
name: "shield:publishing:review-release-tool-publish",
|
|
8389
|
+
tool: "bash",
|
|
8036
8390
|
conditions: [
|
|
8037
8391
|
{
|
|
8038
8392
|
field: "command",
|
|
8039
8393
|
op: "matches",
|
|
8040
|
-
value: "(
|
|
8394
|
+
value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
8041
8395
|
flags: "i"
|
|
8042
8396
|
}
|
|
8043
8397
|
],
|
|
8044
|
-
verdict: "
|
|
8045
|
-
reason: "
|
|
8398
|
+
verdict: "review",
|
|
8399
|
+
reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
|
|
8046
8400
|
},
|
|
8047
8401
|
{
|
|
8048
|
-
name: "shield:
|
|
8049
|
-
tool: "
|
|
8402
|
+
name: "shield:publishing:review-cargo-publish",
|
|
8403
|
+
tool: "bash",
|
|
8050
8404
|
conditions: [
|
|
8051
8405
|
{
|
|
8052
8406
|
field: "command",
|
|
8053
8407
|
op: "matches",
|
|
8054
|
-
value: "(
|
|
8408
|
+
value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
|
|
8055
8409
|
flags: "i"
|
|
8056
8410
|
}
|
|
8057
8411
|
],
|
|
8058
|
-
verdict: "
|
|
8059
|
-
reason: "
|
|
8412
|
+
verdict: "review",
|
|
8413
|
+
reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
|
|
8060
8414
|
},
|
|
8061
8415
|
{
|
|
8062
|
-
name: "shield:
|
|
8063
|
-
tool: "
|
|
8416
|
+
name: "shield:publishing:review-gem-push",
|
|
8417
|
+
tool: "bash",
|
|
8064
8418
|
conditions: [
|
|
8065
8419
|
{
|
|
8066
8420
|
field: "command",
|
|
8067
8421
|
op: "matches",
|
|
8068
|
-
value: "(
|
|
8422
|
+
value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
|
|
8069
8423
|
flags: "i"
|
|
8070
8424
|
}
|
|
8071
8425
|
],
|
|
8072
8426
|
verdict: "review",
|
|
8073
|
-
reason: "
|
|
8427
|
+
reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
|
|
8074
8428
|
},
|
|
8075
8429
|
{
|
|
8076
|
-
name: "shield:
|
|
8077
|
-
tool: "
|
|
8430
|
+
name: "shield:publishing:review-docker-push",
|
|
8431
|
+
tool: "bash",
|
|
8078
8432
|
conditions: [
|
|
8079
8433
|
{
|
|
8080
8434
|
field: "command",
|
|
8081
8435
|
op: "matches",
|
|
8082
|
-
value: "(
|
|
8436
|
+
value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
|
|
8083
8437
|
flags: "i"
|
|
8084
8438
|
}
|
|
8085
8439
|
],
|
|
8086
8440
|
verdict: "review",
|
|
8087
|
-
reason: "
|
|
8441
|
+
reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
|
|
8442
|
+
},
|
|
8443
|
+
{
|
|
8444
|
+
name: "shield:publishing:review-gh-release",
|
|
8445
|
+
tool: "bash",
|
|
8446
|
+
conditions: [
|
|
8447
|
+
{
|
|
8448
|
+
field: "command",
|
|
8449
|
+
op: "matches",
|
|
8450
|
+
value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
|
|
8451
|
+
flags: "i"
|
|
8452
|
+
}
|
|
8453
|
+
],
|
|
8454
|
+
verdict: "review",
|
|
8455
|
+
reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
|
|
8088
8456
|
}
|
|
8089
8457
|
],
|
|
8090
|
-
dangerousWords: [
|
|
8458
|
+
dangerousWords: []
|
|
8459
|
+
};
|
|
8460
|
+
ACTION_TEMPLATES = {
|
|
8461
|
+
finance: finance_default.smartRules,
|
|
8462
|
+
identity: identity_default.smartRules,
|
|
8463
|
+
publishing: publishing_default.smartRules
|
|
8464
|
+
};
|
|
8465
|
+
ACTION_CHECKS = {
|
|
8466
|
+
finance: { checkId: "actions.money", slug: "money", label: "Money check" },
|
|
8467
|
+
identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
|
|
8468
|
+
publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
|
|
8091
8469
|
};
|
|
8470
|
+
GAP = "[^;&|]{0,200}?";
|
|
8471
|
+
prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
|
|
8472
|
+
httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
|
|
8473
|
+
httpWrite = (methods) => ({
|
|
8474
|
+
field: "command",
|
|
8475
|
+
op: "matches",
|
|
8476
|
+
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)`
|
|
8477
|
+
});
|
|
8478
|
+
HTTP_WRITE = httpWrite("POST|DELETE");
|
|
8479
|
+
HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
|
|
8480
|
+
NOT_STRIPE_TEST = {
|
|
8481
|
+
field: "command",
|
|
8482
|
+
op: "notMatches",
|
|
8483
|
+
value: "\\b[sr]k_test_"
|
|
8484
|
+
};
|
|
8485
|
+
postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
|
|
8486
|
+
STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
|
|
8487
|
+
APP_SHIELD_SPECS = [
|
|
8488
|
+
{
|
|
8489
|
+
name: "stripe",
|
|
8490
|
+
label: "Stripe",
|
|
8491
|
+
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.",
|
|
8492
|
+
template: "finance",
|
|
8493
|
+
server: "*stripe*",
|
|
8494
|
+
doors: [
|
|
8495
|
+
{
|
|
8496
|
+
name: "shield:stripe:review-cli",
|
|
8497
|
+
tool: "bash",
|
|
8498
|
+
conditions: [
|
|
8499
|
+
{
|
|
8500
|
+
field: "command",
|
|
8501
|
+
op: "matches",
|
|
8502
|
+
value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
|
|
8503
|
+
flags: "i"
|
|
8504
|
+
},
|
|
8505
|
+
{
|
|
8506
|
+
field: "command",
|
|
8507
|
+
op: "matches",
|
|
8508
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8509
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8510
|
+
}
|
|
8511
|
+
],
|
|
8512
|
+
conditionMode: "all",
|
|
8513
|
+
verdict: "review",
|
|
8514
|
+
reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
|
|
8515
|
+
},
|
|
8516
|
+
{
|
|
8517
|
+
name: "shield:stripe:review-cli-request",
|
|
8518
|
+
tool: "bash",
|
|
8519
|
+
conditions: [
|
|
8520
|
+
{
|
|
8521
|
+
field: "command",
|
|
8522
|
+
op: "matches",
|
|
8523
|
+
value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
|
|
8524
|
+
flags: "i"
|
|
8525
|
+
},
|
|
8526
|
+
{
|
|
8527
|
+
field: "command",
|
|
8528
|
+
op: "matches",
|
|
8529
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8530
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8531
|
+
}
|
|
8532
|
+
],
|
|
8533
|
+
conditionMode: "all",
|
|
8534
|
+
verdict: "review",
|
|
8535
|
+
reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
|
|
8536
|
+
},
|
|
8537
|
+
{
|
|
8538
|
+
name: "shield:stripe:review-api",
|
|
8539
|
+
tool: "bash",
|
|
8540
|
+
conditions: [
|
|
8541
|
+
{
|
|
8542
|
+
field: "command",
|
|
8543
|
+
op: "matches",
|
|
8544
|
+
value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
|
|
8545
|
+
flags: "i"
|
|
8546
|
+
},
|
|
8547
|
+
HTTP_WRITE,
|
|
8548
|
+
NOT_STRIPE_TEST
|
|
8549
|
+
],
|
|
8550
|
+
conditionMode: "all",
|
|
8551
|
+
verdict: "review",
|
|
8552
|
+
reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
|
|
8553
|
+
},
|
|
8554
|
+
{
|
|
8555
|
+
name: "shield:stripe:review-code",
|
|
8556
|
+
tool: "bash",
|
|
8557
|
+
conditions: [
|
|
8558
|
+
{
|
|
8559
|
+
field: "command",
|
|
8560
|
+
op: "matches",
|
|
8561
|
+
// Python: stripe.Refund.create(...)
|
|
8562
|
+
value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
|
|
8563
|
+
},
|
|
8564
|
+
NOT_STRIPE_TEST
|
|
8565
|
+
],
|
|
8566
|
+
conditionMode: "all",
|
|
8567
|
+
verdict: "review",
|
|
8568
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8569
|
+
},
|
|
8570
|
+
{
|
|
8571
|
+
name: "shield:stripe:review-code-node",
|
|
8572
|
+
tool: "bash",
|
|
8573
|
+
conditions: [
|
|
8574
|
+
{
|
|
8575
|
+
field: "command",
|
|
8576
|
+
op: "matches",
|
|
8577
|
+
// Node: stripe.refunds.create(...)
|
|
8578
|
+
value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
|
|
8579
|
+
},
|
|
8580
|
+
NOT_STRIPE_TEST
|
|
8581
|
+
],
|
|
8582
|
+
conditionMode: "all",
|
|
8583
|
+
verdict: "review",
|
|
8584
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8585
|
+
},
|
|
8586
|
+
{
|
|
8587
|
+
name: "shield:stripe:review-code-http",
|
|
8588
|
+
tool: "bash",
|
|
8589
|
+
conditions: [
|
|
8590
|
+
{
|
|
8591
|
+
field: "command",
|
|
8592
|
+
op: "matches",
|
|
8593
|
+
// requests.post("https://api.stripe.com/v1/refunds", ...)
|
|
8594
|
+
value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
|
|
8595
|
+
},
|
|
8596
|
+
NOT_STRIPE_TEST
|
|
8597
|
+
],
|
|
8598
|
+
conditionMode: "all",
|
|
8599
|
+
verdict: "review",
|
|
8600
|
+
reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
|
|
8601
|
+
}
|
|
8602
|
+
]
|
|
8603
|
+
},
|
|
8604
|
+
{
|
|
8605
|
+
name: "paypal",
|
|
8606
|
+
label: "PayPal",
|
|
8607
|
+
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.",
|
|
8608
|
+
template: "finance",
|
|
8609
|
+
server: "*paypal*",
|
|
8610
|
+
doors: [
|
|
8611
|
+
{
|
|
8612
|
+
name: "shield:paypal:review-api",
|
|
8613
|
+
tool: "bash",
|
|
8614
|
+
conditions: [
|
|
8615
|
+
{
|
|
8616
|
+
field: "command",
|
|
8617
|
+
op: "matches",
|
|
8618
|
+
// The live hosts only: api.paypal.com and api-m.paypal.com. The
|
|
8619
|
+
// sandbox (api-m.sandbox.paypal.com) moves no money.
|
|
8620
|
+
value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
|
|
8621
|
+
flags: "i"
|
|
8622
|
+
},
|
|
8623
|
+
{
|
|
8624
|
+
field: "command",
|
|
8625
|
+
op: "matches",
|
|
8626
|
+
// The endpoints that move money.
|
|
8627
|
+
value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
|
|
8628
|
+
flags: "i"
|
|
8629
|
+
},
|
|
8630
|
+
HTTP_WRITE
|
|
8631
|
+
],
|
|
8632
|
+
conditionMode: "all",
|
|
8633
|
+
verdict: "review",
|
|
8634
|
+
reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
|
|
8635
|
+
},
|
|
8636
|
+
{
|
|
8637
|
+
name: "shield:paypal:review-code",
|
|
8638
|
+
tool: "bash",
|
|
8639
|
+
conditions: [
|
|
8640
|
+
{
|
|
8641
|
+
field: "command",
|
|
8642
|
+
op: "matches",
|
|
8643
|
+
// The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
|
|
8644
|
+
// Python paypalrestsdk.
|
|
8645
|
+
value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
|
|
8646
|
+
}
|
|
8647
|
+
],
|
|
8648
|
+
verdict: "review",
|
|
8649
|
+
reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
|
|
8650
|
+
},
|
|
8651
|
+
{
|
|
8652
|
+
name: "shield:paypal:review-code-http",
|
|
8653
|
+
tool: "bash",
|
|
8654
|
+
conditions: [
|
|
8655
|
+
{
|
|
8656
|
+
field: "command",
|
|
8657
|
+
op: "matches",
|
|
8658
|
+
// The endpoints that move money; the token call every script
|
|
8659
|
+
// starts with (/v1/oauth2/token) is not one.
|
|
8660
|
+
value: postTo(
|
|
8661
|
+
"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)"
|
|
8662
|
+
)
|
|
8663
|
+
}
|
|
8664
|
+
],
|
|
8665
|
+
verdict: "review",
|
|
8666
|
+
reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
|
|
8667
|
+
}
|
|
8668
|
+
]
|
|
8669
|
+
},
|
|
8670
|
+
{
|
|
8671
|
+
name: "square",
|
|
8672
|
+
label: "Square",
|
|
8673
|
+
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.",
|
|
8674
|
+
template: "finance",
|
|
8675
|
+
// Square's servers, never Squarespace (a website builder).
|
|
8676
|
+
server: "{*square,*square[-_]*,*squareup*}",
|
|
8677
|
+
mcpTools: [
|
|
8678
|
+
{
|
|
8679
|
+
// Square's official MCP server has one tool for every API call; the
|
|
8680
|
+
// service and method name what it does.
|
|
8681
|
+
name: "shield:square:review-mcp-request",
|
|
8682
|
+
tool: "{,mcp__*__}make_api_request",
|
|
8683
|
+
conditions: [
|
|
8684
|
+
{
|
|
8685
|
+
field: "service",
|
|
8686
|
+
op: "matches",
|
|
8687
|
+
value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
|
|
8688
|
+
flags: "i"
|
|
8689
|
+
},
|
|
8690
|
+
{
|
|
8691
|
+
field: "method",
|
|
8692
|
+
op: "notMatches",
|
|
8693
|
+
value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
|
|
8694
|
+
flags: "i"
|
|
8695
|
+
}
|
|
8696
|
+
],
|
|
8697
|
+
conditionMode: "all",
|
|
8698
|
+
verdict: "review",
|
|
8699
|
+
reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
|
|
8700
|
+
}
|
|
8701
|
+
],
|
|
8702
|
+
doors: [
|
|
8703
|
+
{
|
|
8704
|
+
name: "shield:square:review-api",
|
|
8705
|
+
tool: "bash",
|
|
8706
|
+
conditions: [
|
|
8707
|
+
{
|
|
8708
|
+
field: "command",
|
|
8709
|
+
op: "matches",
|
|
8710
|
+
// The live host only; the sandbox is connect.squareupsandbox.com.
|
|
8711
|
+
value: httpTo(
|
|
8712
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8713
|
+
),
|
|
8714
|
+
flags: "i"
|
|
8715
|
+
},
|
|
8716
|
+
HTTP_WRITE_ANY
|
|
8717
|
+
],
|
|
8718
|
+
conditionMode: "all",
|
|
8719
|
+
verdict: "review",
|
|
8720
|
+
reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
|
|
8721
|
+
},
|
|
8722
|
+
{
|
|
8723
|
+
name: "shield:square:review-code",
|
|
8724
|
+
tool: "bash",
|
|
8725
|
+
conditions: [
|
|
8726
|
+
{
|
|
8727
|
+
field: "command",
|
|
8728
|
+
op: "matches",
|
|
8729
|
+
// The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
|
|
8730
|
+
value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
|
|
8731
|
+
},
|
|
8732
|
+
{
|
|
8733
|
+
field: "command",
|
|
8734
|
+
op: "matches",
|
|
8735
|
+
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*\\("
|
|
8736
|
+
}
|
|
8737
|
+
],
|
|
8738
|
+
conditionMode: "all",
|
|
8739
|
+
verdict: "review",
|
|
8740
|
+
reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
|
|
8741
|
+
},
|
|
8742
|
+
{
|
|
8743
|
+
name: "shield:square:review-code-http",
|
|
8744
|
+
tool: "bash",
|
|
8745
|
+
conditions: [
|
|
8746
|
+
{
|
|
8747
|
+
field: "command",
|
|
8748
|
+
op: "matches",
|
|
8749
|
+
value: postTo(
|
|
8750
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8751
|
+
)
|
|
8752
|
+
}
|
|
8753
|
+
],
|
|
8754
|
+
verdict: "review",
|
|
8755
|
+
reason: "This code posts to the Square API. node9 asks before money moves (Square)."
|
|
8756
|
+
}
|
|
8757
|
+
]
|
|
8758
|
+
},
|
|
8759
|
+
{
|
|
8760
|
+
name: "plaid",
|
|
8761
|
+
label: "Plaid",
|
|
8762
|
+
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.",
|
|
8763
|
+
template: "finance",
|
|
8764
|
+
server: "*plaid*",
|
|
8765
|
+
doors: [
|
|
8766
|
+
{
|
|
8767
|
+
name: "shield:plaid:review-api",
|
|
8768
|
+
tool: "bash",
|
|
8769
|
+
conditions: [
|
|
8770
|
+
{
|
|
8771
|
+
field: "command",
|
|
8772
|
+
op: "matches",
|
|
8773
|
+
// Every Plaid endpoint is a POST, so the path is what tells a
|
|
8774
|
+
// write; the live host only, never sandbox.plaid.com. A script
|
|
8775
|
+
// that posts to the URL meets this door too.
|
|
8776
|
+
value: httpTo(
|
|
8777
|
+
"production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
|
|
8778
|
+
),
|
|
8779
|
+
flags: "i"
|
|
8780
|
+
}
|
|
8781
|
+
],
|
|
8782
|
+
verdict: "review",
|
|
8783
|
+
reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
|
|
8784
|
+
},
|
|
8785
|
+
{
|
|
8786
|
+
name: "shield:plaid:review-code",
|
|
8787
|
+
tool: "bash",
|
|
8788
|
+
conditions: [
|
|
8789
|
+
{ field: "command", op: "matches", value: "\\bplaid", flags: "i" },
|
|
8790
|
+
{
|
|
8791
|
+
field: "command",
|
|
8792
|
+
op: "matches",
|
|
8793
|
+
value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
|
|
8794
|
+
}
|
|
8795
|
+
],
|
|
8796
|
+
conditionMode: "all",
|
|
8797
|
+
verdict: "review",
|
|
8798
|
+
reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
|
|
8799
|
+
}
|
|
8800
|
+
]
|
|
8801
|
+
},
|
|
8802
|
+
{
|
|
8803
|
+
name: "okta",
|
|
8804
|
+
label: "Okta",
|
|
8805
|
+
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.",
|
|
8806
|
+
template: "identity",
|
|
8807
|
+
server: "*okta*",
|
|
8808
|
+
doors: [
|
|
8809
|
+
{
|
|
8810
|
+
name: "shield:okta:review-api",
|
|
8811
|
+
tool: "bash",
|
|
8812
|
+
conditions: [
|
|
8813
|
+
{
|
|
8814
|
+
field: "command",
|
|
8815
|
+
op: "matches",
|
|
8816
|
+
// The management API only: sign-in (/api/v1/authn, /sessions,
|
|
8817
|
+
// /oauth2) is not under these paths.
|
|
8818
|
+
value: httpTo(
|
|
8819
|
+
"[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
|
|
8820
|
+
),
|
|
8821
|
+
flags: "i"
|
|
8822
|
+
},
|
|
8823
|
+
HTTP_WRITE_ANY
|
|
8824
|
+
],
|
|
8825
|
+
conditionMode: "all",
|
|
8826
|
+
verdict: "review",
|
|
8827
|
+
reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
|
|
8828
|
+
},
|
|
8829
|
+
{
|
|
8830
|
+
name: "shield:okta:review-code",
|
|
8831
|
+
tool: "bash",
|
|
8832
|
+
conditions: [
|
|
8833
|
+
{
|
|
8834
|
+
field: "command",
|
|
8835
|
+
op: "matches",
|
|
8836
|
+
// The Okta SDK in the script: a method named createUser alone is
|
|
8837
|
+
// in half the applications ever written.
|
|
8838
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8839
|
+
},
|
|
8840
|
+
{
|
|
8841
|
+
field: "command",
|
|
8842
|
+
op: "matches",
|
|
8843
|
+
value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
|
|
8844
|
+
}
|
|
8845
|
+
],
|
|
8846
|
+
conditionMode: "all",
|
|
8847
|
+
verdict: "review",
|
|
8848
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8849
|
+
},
|
|
8850
|
+
{
|
|
8851
|
+
name: "shield:okta:review-code-py",
|
|
8852
|
+
tool: "bash",
|
|
8853
|
+
conditions: [
|
|
8854
|
+
{
|
|
8855
|
+
field: "command",
|
|
8856
|
+
op: "matches",
|
|
8857
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8858
|
+
},
|
|
8859
|
+
{
|
|
8860
|
+
field: "command",
|
|
8861
|
+
op: "matches",
|
|
8862
|
+
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*\\("
|
|
8863
|
+
}
|
|
8864
|
+
],
|
|
8865
|
+
conditionMode: "all",
|
|
8866
|
+
verdict: "review",
|
|
8867
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8868
|
+
}
|
|
8869
|
+
]
|
|
8870
|
+
},
|
|
8871
|
+
{
|
|
8872
|
+
name: "auth0",
|
|
8873
|
+
label: "Auth0",
|
|
8874
|
+
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.",
|
|
8875
|
+
template: "identity",
|
|
8876
|
+
server: "*auth0*",
|
|
8877
|
+
mcpTools: [
|
|
8878
|
+
{
|
|
8879
|
+
// Auth0's official MCP server: applications (their secrets and
|
|
8880
|
+
// callbacks), APIs and their scopes, and Actions (code that runs at
|
|
8881
|
+
// every sign-in).
|
|
8882
|
+
name: "shield:auth0:review-tenant-config",
|
|
8883
|
+
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}",
|
|
8884
|
+
conditions: [],
|
|
8885
|
+
verdict: "review",
|
|
8886
|
+
reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
|
|
8887
|
+
}
|
|
8888
|
+
],
|
|
8889
|
+
doors: [
|
|
8890
|
+
{
|
|
8891
|
+
name: "shield:auth0:review-cli",
|
|
8892
|
+
tool: "bash",
|
|
8893
|
+
conditions: [
|
|
8894
|
+
{
|
|
8895
|
+
field: "command",
|
|
8896
|
+
op: "matches",
|
|
8897
|
+
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-])",
|
|
8898
|
+
flags: "i"
|
|
8899
|
+
}
|
|
8900
|
+
],
|
|
8901
|
+
verdict: "review",
|
|
8902
|
+
reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8903
|
+
},
|
|
8904
|
+
{
|
|
8905
|
+
name: "shield:auth0:review-api",
|
|
8906
|
+
tool: "bash",
|
|
8907
|
+
conditions: [
|
|
8908
|
+
{
|
|
8909
|
+
field: "command",
|
|
8910
|
+
op: "matches",
|
|
8911
|
+
// The Management API only; /oauth/token is signing in.
|
|
8912
|
+
value: httpTo(
|
|
8913
|
+
"\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
|
|
8914
|
+
),
|
|
8915
|
+
flags: "i"
|
|
8916
|
+
},
|
|
8917
|
+
HTTP_WRITE_ANY
|
|
8918
|
+
],
|
|
8919
|
+
conditionMode: "all",
|
|
8920
|
+
verdict: "review",
|
|
8921
|
+
reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8922
|
+
},
|
|
8923
|
+
{
|
|
8924
|
+
name: "shield:auth0:review-code",
|
|
8925
|
+
tool: "bash",
|
|
8926
|
+
conditions: [
|
|
8927
|
+
{
|
|
8928
|
+
field: "command",
|
|
8929
|
+
op: "matches",
|
|
8930
|
+
// The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
|
|
8931
|
+
value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
|
|
8932
|
+
},
|
|
8933
|
+
{
|
|
8934
|
+
field: "command",
|
|
8935
|
+
op: "matches",
|
|
8936
|
+
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*\\("
|
|
8937
|
+
}
|
|
8938
|
+
],
|
|
8939
|
+
conditionMode: "all",
|
|
8940
|
+
verdict: "review",
|
|
8941
|
+
reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
|
|
8942
|
+
}
|
|
8943
|
+
]
|
|
8944
|
+
}
|
|
8945
|
+
];
|
|
8946
|
+
APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
|
|
8947
|
+
APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
|
|
8948
|
+
APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
|
|
8949
|
+
GENERAL_ACTION_RULES = buildGeneralRules();
|
|
8092
8950
|
BUILTIN_SHIELDS = {
|
|
8093
8951
|
[aws_default.name]: aws_default,
|
|
8094
8952
|
[bash_safe_default.name]: bash_safe_default,
|
|
8095
8953
|
[docker_default.name]: docker_default,
|
|
8096
8954
|
[filesystem_default.name]: filesystem_default,
|
|
8097
|
-
// Dangerous actions as category shields (node9Firewall#385): an MCP app's
|
|
8098
|
-
// money, credential and publishing tools, asked about whatever the request
|
|
8099
|
-
// said. One glob per rule covers the bare name the gateway sends and the
|
|
8100
|
-
// mcp__<server>__ name the agent hook sends.
|
|
8101
|
-
[finance_default.name]: finance_default,
|
|
8102
8955
|
[github_default.name]: github_default,
|
|
8103
|
-
[identity_default.name]: identity_default,
|
|
8104
8956
|
[k8s_default.name]: k8s_default,
|
|
8105
8957
|
[mongodb_default.name]: mongodb_default,
|
|
8106
8958
|
[postgres_default.name]: postgres_default,
|
|
8107
8959
|
[project_jail_default.name]: project_jail_default,
|
|
8108
|
-
[
|
|
8109
|
-
|
|
8960
|
+
[redis_default.name]: redis_default,
|
|
8961
|
+
// App shields generated from the action templates (node9Firewall#390): one
|
|
8962
|
+
// per app, every door (MCP server, CLI, API, library), off by default.
|
|
8963
|
+
...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
|
|
8110
8964
|
};
|
|
8111
8965
|
assertBuiltinShieldRegexesAreSafe();
|
|
8112
8966
|
SSRF_MAX_HOST = 253;
|
|
@@ -8162,9 +9016,9 @@ var init_dist = __esm({
|
|
|
8162
9016
|
},
|
|
8163
9017
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
8164
9018
|
"commands.inline-exec": {
|
|
8165
|
-
plain: "The agent runs a small program written inside the command itself.",
|
|
9019
|
+
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.",
|
|
8166
9020
|
example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
|
|
8167
|
-
advice: "
|
|
9021
|
+
advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
|
|
8168
9022
|
},
|
|
8169
9023
|
"commands.eval-dynamic": {
|
|
8170
9024
|
plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
|
|
@@ -8196,6 +9050,21 @@ var init_dist = __esm({
|
|
|
8196
9050
|
example: "chmod 777 deploy.sh",
|
|
8197
9051
|
advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
|
|
8198
9052
|
},
|
|
9053
|
+
"actions.money": {
|
|
9054
|
+
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.",
|
|
9055
|
+
example: "refund_order on a server called acme-billing",
|
|
9056
|
+
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."
|
|
9057
|
+
},
|
|
9058
|
+
"actions.access": {
|
|
9059
|
+
plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
|
|
9060
|
+
example: "invite_user or grant_role on a company admin server",
|
|
9061
|
+
advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
|
|
9062
|
+
},
|
|
9063
|
+
"actions.publish": {
|
|
9064
|
+
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.",
|
|
9065
|
+
example: "create_release or deploy_to_production on a company MCP server",
|
|
9066
|
+
advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
|
|
9067
|
+
},
|
|
8199
9068
|
"commands.sudo": {
|
|
8200
9069
|
plain: "The agent runs a command as the administrator (root).",
|
|
8201
9070
|
example: "sudo apt-get install -y nginx",
|
|
@@ -8356,11 +9225,57 @@ var init_dist = __esm({
|
|
|
8356
9225
|
advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
|
|
8357
9226
|
},
|
|
8358
9227
|
"loading.malicious-package": {
|
|
8359
|
-
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.",
|
|
9228
|
+
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.",
|
|
8360
9229
|
example: "npm install of a package flagged as malware",
|
|
8361
9230
|
advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
|
|
8362
9231
|
}
|
|
8363
9232
|
};
|
|
9233
|
+
LEVELS = ["audit", "soft", "standard", "strict", "block-all"];
|
|
9234
|
+
MODE_VALUES = ["observe", ...LEVELS];
|
|
9235
|
+
SOFT = {
|
|
9236
|
+
"commands.inline-exec": "log",
|
|
9237
|
+
"commands.eval-dynamic": "log",
|
|
9238
|
+
"commands.rm": "log",
|
|
9239
|
+
"commands.chmod": "log",
|
|
9240
|
+
"commands.sudo": "log",
|
|
9241
|
+
"commands.git-destructive": "log",
|
|
9242
|
+
"commands.sql-ddl": "log",
|
|
9243
|
+
"commands.sql-no-where": "log",
|
|
9244
|
+
"commands.temp-binary": "log",
|
|
9245
|
+
"commands.disk-destroy": "log",
|
|
9246
|
+
"commands.dangerous-word": "log",
|
|
9247
|
+
"files.system-dirs": "log",
|
|
9248
|
+
"files.shell-startup": "log",
|
|
9249
|
+
"files.password-hashes": "log",
|
|
9250
|
+
"data.pipe-chain": "log",
|
|
9251
|
+
"behavior.untrusted-content": "log"
|
|
9252
|
+
};
|
|
9253
|
+
STRICT = {
|
|
9254
|
+
"commands.sql-ddl": "block",
|
|
9255
|
+
"commands.disk-destroy": "block",
|
|
9256
|
+
"commands.node9-disarm": "block",
|
|
9257
|
+
"files.system-dirs": "block",
|
|
9258
|
+
"files.shell-startup": "block",
|
|
9259
|
+
"files.password-hashes": "block",
|
|
9260
|
+
"data.secrets-weak": "block",
|
|
9261
|
+
"data.pipe-chain": "block",
|
|
9262
|
+
"network.unknown-host": "review",
|
|
9263
|
+
"network.internal-addresses": "block",
|
|
9264
|
+
"behavior.prompt-injection": "log",
|
|
9265
|
+
"loading.skill-tamper": "block"
|
|
9266
|
+
};
|
|
9267
|
+
BLOCK_ALL = {
|
|
9268
|
+
...STRICT,
|
|
9269
|
+
"commands.unknown": "review",
|
|
9270
|
+
"commands.temp-binary": "block"
|
|
9271
|
+
};
|
|
9272
|
+
LEVEL_CHECKS = {
|
|
9273
|
+
audit: {},
|
|
9274
|
+
soft: SOFT,
|
|
9275
|
+
standard: {},
|
|
9276
|
+
strict: STRICT,
|
|
9277
|
+
"block-all": BLOCK_ALL
|
|
9278
|
+
};
|
|
8364
9279
|
VERDICTS = ["off", "log", "review", "block"];
|
|
8365
9280
|
CHECK_VERDICT_RANK = { off: 0, log: 1, review: 2, block: 3 };
|
|
8366
9281
|
ALL = VERDICTS;
|
|
@@ -8408,7 +9323,7 @@ var init_dist = __esm({
|
|
|
8408
9323
|
id: "commands.inline-exec",
|
|
8409
9324
|
group: "commands",
|
|
8410
9325
|
title: "Inline code execution",
|
|
8411
|
-
catches: "
|
|
9326
|
+
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",
|
|
8412
9327
|
values: ALL,
|
|
8413
9328
|
defaultValue: "review",
|
|
8414
9329
|
legacy: ["Node9 Standard (Inline Execution)"]
|
|
@@ -8765,6 +9680,38 @@ var init_dist = __esm({
|
|
|
8765
9680
|
values: ALL,
|
|
8766
9681
|
defaultValue: "block",
|
|
8767
9682
|
legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
|
|
9683
|
+
},
|
|
9684
|
+
// ── Actions in apps (node9Firewall#390) ──────────────────────────────────
|
|
9685
|
+
// The safety net under the app shields: an action that moves money, changes
|
|
9686
|
+
// access or publishes, on an MCP server no app shield claims (an internal
|
|
9687
|
+
// billing server, a vendor with no shield, Zapier). Review by default; the
|
|
9688
|
+
// rules come from the action templates (shields/apps.ts).
|
|
9689
|
+
{
|
|
9690
|
+
id: "actions.money",
|
|
9691
|
+
group: "actions",
|
|
9692
|
+
title: "Money moves",
|
|
9693
|
+
catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
|
|
9694
|
+
values: ALL,
|
|
9695
|
+
defaultValue: "review",
|
|
9696
|
+
legacy: generalActionRuleNames("finance")
|
|
9697
|
+
},
|
|
9698
|
+
{
|
|
9699
|
+
id: "actions.access",
|
|
9700
|
+
group: "actions",
|
|
9701
|
+
title: "Accounts and access change",
|
|
9702
|
+
catches: "a password, key, role, permission or user changed through an app with no protection of its own",
|
|
9703
|
+
values: ALL,
|
|
9704
|
+
defaultValue: "review",
|
|
9705
|
+
legacy: generalActionRuleNames("identity")
|
|
9706
|
+
},
|
|
9707
|
+
{
|
|
9708
|
+
id: "actions.publish",
|
|
9709
|
+
group: "actions",
|
|
9710
|
+
title: "Something is published",
|
|
9711
|
+
catches: "a release, package, deploy or public post through an app with no protection of its own",
|
|
9712
|
+
values: ALL,
|
|
9713
|
+
defaultValue: "review",
|
|
9714
|
+
legacy: generalActionRuleNames("publishing")
|
|
8768
9715
|
}
|
|
8769
9716
|
];
|
|
8770
9717
|
SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
|
|
@@ -8785,7 +9732,7 @@ var init_dist = __esm({
|
|
|
8785
9732
|
"commands.rm": (s) => knob(s.commandChecks?.rmAdvisory),
|
|
8786
9733
|
"commands.chmod": (s) => knob(s.commandChecks?.chmod),
|
|
8787
9734
|
"commands.sql-ddl": (s) => knob(s.commandChecks?.sqlDdl),
|
|
8788
|
-
"commands.unknown": (s) => s.mode === void 0 ? void 0 : s.mode
|
|
9735
|
+
"commands.unknown": (s) => s.mode === void 0 ? void 0 : isBlockAll(s.mode) ? "review" : "off",
|
|
8789
9736
|
"data.secrets": (s) => enabledOr(s.dlp?.enabled, "block"),
|
|
8790
9737
|
"data.secrets-weak": (s) => {
|
|
8791
9738
|
if (s.dlp?.enabled === false) return "off";
|
|
@@ -8893,6 +9840,217 @@ var init_dist = __esm({
|
|
|
8893
9840
|
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
8894
9841
|
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
8895
9842
|
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
9843
|
+
SYSTEM_AUTH_FILES = [
|
|
9844
|
+
"/etc/passwd",
|
|
9845
|
+
"/etc/shadow",
|
|
9846
|
+
"/etc/group",
|
|
9847
|
+
"/etc/gshadow",
|
|
9848
|
+
"/etc/sudoers"
|
|
9849
|
+
];
|
|
9850
|
+
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9851
|
+
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9852
|
+
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9853
|
+
SYSTEM_DIRS = [
|
|
9854
|
+
"/usr",
|
|
9855
|
+
"/etc",
|
|
9856
|
+
"/boot",
|
|
9857
|
+
"/bin",
|
|
9858
|
+
"/sbin",
|
|
9859
|
+
"/lib",
|
|
9860
|
+
"/lib64",
|
|
9861
|
+
"/var/log"
|
|
9862
|
+
];
|
|
9863
|
+
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
9864
|
+
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
9865
|
+
AGENT_INSTRUCTION_FILES = [
|
|
9866
|
+
".gitlab-ci.yml",
|
|
9867
|
+
"CLAUDE.md",
|
|
9868
|
+
"AGENTS.md",
|
|
9869
|
+
".cursorrules",
|
|
9870
|
+
".mcp.json",
|
|
9871
|
+
".vscode/settings.json",
|
|
9872
|
+
"package.json"
|
|
9873
|
+
];
|
|
9874
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
9875
|
+
".github/workflows",
|
|
9876
|
+
".claude",
|
|
9877
|
+
".cursor",
|
|
9878
|
+
".git/hooks",
|
|
9879
|
+
".husky"
|
|
9880
|
+
];
|
|
9881
|
+
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
9882
|
+
MAX_INLINE_CODE = 64 * 1024;
|
|
9883
|
+
WHAT = {
|
|
9884
|
+
deletes: "deletes files",
|
|
9885
|
+
"writes-outside": "writes a file outside the project",
|
|
9886
|
+
sends: "sends data over the network",
|
|
9887
|
+
listens: "opens a listening socket or a shell",
|
|
9888
|
+
"runs-commands": "runs other commands or processes",
|
|
9889
|
+
"reads-protected": "reads a protected file",
|
|
9890
|
+
"builds-code": "runs code it builds at run time",
|
|
9891
|
+
permissions: "changes permissions or ownership",
|
|
9892
|
+
monitors: "captures the screen, the clipboard or the keyboard",
|
|
9893
|
+
amplifies: "writes or sends in an endless loop"
|
|
9894
|
+
};
|
|
9895
|
+
OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
|
|
9896
|
+
pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
|
|
9897
|
+
either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
|
|
9898
|
+
PYTHON = [
|
|
9899
|
+
[
|
|
9900
|
+
"deletes",
|
|
9901
|
+
/\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/
|
|
9902
|
+
],
|
|
9903
|
+
[
|
|
9904
|
+
"writes-outside",
|
|
9905
|
+
new RegExp(
|
|
9906
|
+
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}`
|
|
9907
|
+
)
|
|
9908
|
+
],
|
|
9909
|
+
[
|
|
9910
|
+
"sends",
|
|
9911
|
+
either(
|
|
9912
|
+
pyImport(
|
|
9913
|
+
String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
|
|
9914
|
+
),
|
|
9915
|
+
/\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
|
|
9916
|
+
)
|
|
9917
|
+
],
|
|
9918
|
+
[
|
|
9919
|
+
"listens",
|
|
9920
|
+
either(
|
|
9921
|
+
pyImport(String.raw`pty|socketserver|http\.server`),
|
|
9922
|
+
/\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
|
|
9923
|
+
)
|
|
9924
|
+
],
|
|
9925
|
+
[
|
|
9926
|
+
"runs-commands",
|
|
9927
|
+
either(
|
|
9928
|
+
pyImport("subprocess|pexpect|commands|psutil"),
|
|
9929
|
+
/\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+\*/
|
|
9930
|
+
)
|
|
9931
|
+
],
|
|
9932
|
+
[
|
|
9933
|
+
"builds-code",
|
|
9934
|
+
/(?<![\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*\[/
|
|
9935
|
+
],
|
|
9936
|
+
[
|
|
9937
|
+
// Spyware shapes: a screenshot, the clipboard, a key logger.
|
|
9938
|
+
"monitors",
|
|
9939
|
+
either(
|
|
9940
|
+
pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
|
|
9941
|
+
/\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
|
|
9942
|
+
)
|
|
9943
|
+
],
|
|
9944
|
+
[
|
|
9945
|
+
"permissions",
|
|
9946
|
+
/\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
|
|
9947
|
+
]
|
|
9948
|
+
];
|
|
9949
|
+
NODE = [
|
|
9950
|
+
[
|
|
9951
|
+
"deletes",
|
|
9952
|
+
/\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
|
|
9953
|
+
],
|
|
9954
|
+
[
|
|
9955
|
+
"writes-outside",
|
|
9956
|
+
new RegExp(
|
|
9957
|
+
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}`
|
|
9958
|
+
)
|
|
9959
|
+
],
|
|
9960
|
+
[
|
|
9961
|
+
"sends",
|
|
9962
|
+
/\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*\(/
|
|
9963
|
+
],
|
|
9964
|
+
["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
|
|
9965
|
+
[
|
|
9966
|
+
"runs-commands",
|
|
9967
|
+
/\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
|
|
9968
|
+
],
|
|
9969
|
+
[
|
|
9970
|
+
"builds-code",
|
|
9971
|
+
/(?<![\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)]/
|
|
9972
|
+
],
|
|
9973
|
+
[
|
|
9974
|
+
"permissions",
|
|
9975
|
+
/\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
|
|
9976
|
+
]
|
|
9977
|
+
];
|
|
9978
|
+
PERL = [
|
|
9979
|
+
["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
|
|
9980
|
+
[
|
|
9981
|
+
"writes-outside",
|
|
9982
|
+
new RegExp(
|
|
9983
|
+
String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
|
|
9984
|
+
)
|
|
9985
|
+
],
|
|
9986
|
+
[
|
|
9987
|
+
"sends",
|
|
9988
|
+
/\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
|
|
9989
|
+
],
|
|
9990
|
+
[
|
|
9991
|
+
"runs-commands",
|
|
9992
|
+
/\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
|
|
9993
|
+
],
|
|
9994
|
+
["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
|
|
9995
|
+
["permissions", /\b(?:chmod|chown)\b/]
|
|
9996
|
+
];
|
|
9997
|
+
RUBY = [
|
|
9998
|
+
[
|
|
9999
|
+
"deletes",
|
|
10000
|
+
/\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
|
|
10001
|
+
],
|
|
10002
|
+
[
|
|
10003
|
+
"writes-outside",
|
|
10004
|
+
new RegExp(
|
|
10005
|
+
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}`
|
|
10006
|
+
)
|
|
10007
|
+
],
|
|
10008
|
+
[
|
|
10009
|
+
"sends",
|
|
10010
|
+
/\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)['"]/
|
|
10011
|
+
],
|
|
10012
|
+
["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
|
|
10013
|
+
[
|
|
10014
|
+
"runs-commands",
|
|
10015
|
+
/(?<![\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/
|
|
10016
|
+
],
|
|
10017
|
+
[
|
|
10018
|
+
"builds-code",
|
|
10019
|
+
/(?<![\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/
|
|
10020
|
+
],
|
|
10021
|
+
["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
|
|
10022
|
+
];
|
|
10023
|
+
PHP = [
|
|
10024
|
+
["deletes", /\b(?:unlink|rmdir)\s*\(/],
|
|
10025
|
+
[
|
|
10026
|
+
"writes-outside",
|
|
10027
|
+
new RegExp(
|
|
10028
|
+
String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
|
|
10029
|
+
)
|
|
10030
|
+
],
|
|
10031
|
+
[
|
|
10032
|
+
"sends",
|
|
10033
|
+
/\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):/
|
|
10034
|
+
],
|
|
10035
|
+
["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
|
|
10036
|
+
[
|
|
10037
|
+
"runs-commands",
|
|
10038
|
+
/\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
|
|
10039
|
+
],
|
|
10040
|
+
[
|
|
10041
|
+
"builds-code",
|
|
10042
|
+
/\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
|
|
10043
|
+
],
|
|
10044
|
+
["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
|
|
10045
|
+
];
|
|
10046
|
+
TABLES = {
|
|
10047
|
+
python: PYTHON,
|
|
10048
|
+
node: NODE,
|
|
10049
|
+
perl: PERL,
|
|
10050
|
+
ruby: RUBY,
|
|
10051
|
+
php: PHP
|
|
10052
|
+
};
|
|
10053
|
+
STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
|
|
8896
10054
|
({ syntax } = mvdanSh);
|
|
8897
10055
|
sharedParser = syntax.NewParser();
|
|
8898
10056
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -8930,6 +10088,10 @@ var init_dist = __esm({
|
|
|
8930
10088
|
AST_CACHE_MAX = 5e3;
|
|
8931
10089
|
astCache = /* @__PURE__ */ new Map();
|
|
8932
10090
|
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
10091
|
+
INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
|
|
10092
|
+
PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
|
|
10093
|
+
ACCESS_VIEW_CACHE_MAX = 1e3;
|
|
10094
|
+
accessViewCache = /* @__PURE__ */ new Map();
|
|
8933
10095
|
MAX_READING_PAYLOADS = 8;
|
|
8934
10096
|
defaultExpansionOps = null;
|
|
8935
10097
|
VARIABLE_READERS = /* @__PURE__ */ new Set([
|
|
@@ -9639,6 +10801,14 @@ var init_dist = __esm({
|
|
|
9639
10801
|
INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
9640
10802
|
MAX_PAYLOAD_NESTING = 8;
|
|
9641
10803
|
payloadNesting = 0;
|
|
10804
|
+
MAX_INLINE_CALLS = 16;
|
|
10805
|
+
INLINE_EXEC_FALLBACK = /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i;
|
|
10806
|
+
UNREADABLE_SOURCE = {
|
|
10807
|
+
pipe: "reads its code from another program through a pipe",
|
|
10808
|
+
file: "reads its code from a file on stdin",
|
|
10809
|
+
variable: "runs code the shell fills in at run time",
|
|
10810
|
+
payload: "runs a shell payload that evaluates, sources or runs further code"
|
|
10811
|
+
};
|
|
9642
10812
|
SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
|
|
9643
10813
|
SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
|
|
9644
10814
|
MAX_SCRIPT_OPERANDS = 8;
|
|
@@ -9838,9 +11008,6 @@ var init_dist = __esm({
|
|
|
9838
11008
|
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
9839
11009
|
NONE = { kind: "none" };
|
|
9840
11010
|
UNKNOWN = { kind: "unknown" };
|
|
9841
|
-
MAX_REGEX_LENGTH = 256;
|
|
9842
|
-
REGEX_CACHE_MAX = 500;
|
|
9843
|
-
regexCache = /* @__PURE__ */ new Map();
|
|
9844
11011
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
9845
11012
|
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9846
11013
|
"bash",
|
|
@@ -9955,45 +11122,6 @@ var init_dist = __esm({
|
|
|
9955
11122
|
(s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
|
|
9956
11123
|
)
|
|
9957
11124
|
);
|
|
9958
|
-
SYSTEM_AUTH_FILES = [
|
|
9959
|
-
"/etc/passwd",
|
|
9960
|
-
"/etc/shadow",
|
|
9961
|
-
"/etc/group",
|
|
9962
|
-
"/etc/gshadow",
|
|
9963
|
-
"/etc/sudoers"
|
|
9964
|
-
];
|
|
9965
|
-
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9966
|
-
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9967
|
-
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9968
|
-
SYSTEM_DIRS = [
|
|
9969
|
-
"/usr",
|
|
9970
|
-
"/etc",
|
|
9971
|
-
"/boot",
|
|
9972
|
-
"/bin",
|
|
9973
|
-
"/sbin",
|
|
9974
|
-
"/lib",
|
|
9975
|
-
"/lib64",
|
|
9976
|
-
"/var/log"
|
|
9977
|
-
];
|
|
9978
|
-
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
9979
|
-
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
9980
|
-
AGENT_INSTRUCTION_FILES = [
|
|
9981
|
-
".gitlab-ci.yml",
|
|
9982
|
-
"CLAUDE.md",
|
|
9983
|
-
"AGENTS.md",
|
|
9984
|
-
".cursorrules",
|
|
9985
|
-
".mcp.json",
|
|
9986
|
-
".vscode/settings.json",
|
|
9987
|
-
"package.json"
|
|
9988
|
-
];
|
|
9989
|
-
AGENT_INSTRUCTION_DIRS = [
|
|
9990
|
-
".github/workflows",
|
|
9991
|
-
".claude",
|
|
9992
|
-
".cursor",
|
|
9993
|
-
".git/hooks",
|
|
9994
|
-
".husky"
|
|
9995
|
-
];
|
|
9996
|
-
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
9997
11125
|
exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
|
|
9998
11126
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
9999
11127
|
BIP39_ENGLISH = `abandon
|
|
@@ -13486,7 +14614,11 @@ zoo`.split("\n");
|
|
|
13486
14614
|
"commands.disk-destroy",
|
|
13487
14615
|
"commands.dangerous-word",
|
|
13488
14616
|
"commands.node9-disarm",
|
|
13489
|
-
"data.pipe-chain"
|
|
14617
|
+
"data.pipe-chain",
|
|
14618
|
+
// The general action checks (node9Firewall#390): smart rules the map governs.
|
|
14619
|
+
"actions.money",
|
|
14620
|
+
"actions.access",
|
|
14621
|
+
"actions.publish"
|
|
13490
14622
|
]);
|
|
13491
14623
|
KNOB_GOVERNED = {
|
|
13492
14624
|
"data.secrets": ["off", "block"],
|
|
@@ -14194,7 +15326,8 @@ var SmartConditionSchema = z.object({
|
|
|
14194
15326
|
}
|
|
14195
15327
|
),
|
|
14196
15328
|
value: z.string().optional(),
|
|
14197
|
-
flags: z.string().optional()
|
|
15329
|
+
flags: z.string().optional(),
|
|
15330
|
+
reading: z.enum(["access"]).optional()
|
|
14198
15331
|
}).refine(
|
|
14199
15332
|
(c) => {
|
|
14200
15333
|
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
@@ -14205,6 +15338,9 @@ var SmartConditionSchema = z.object({
|
|
|
14205
15338
|
var SmartRuleSchema = z.object({
|
|
14206
15339
|
name: z.string().optional(),
|
|
14207
15340
|
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
15341
|
+
// Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
|
|
15342
|
+
server: z.string().min(1).optional(),
|
|
15343
|
+
notServer: z.string().min(1).optional(),
|
|
14208
15344
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
14209
15345
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
14210
15346
|
verdict: z.enum(["allow", "review", "block"], {
|
|
@@ -14225,7 +15361,8 @@ var SmartRuleSchema = z.object({
|
|
|
14225
15361
|
var ConfigFileSchema = z.object({
|
|
14226
15362
|
version: z.string().optional(),
|
|
14227
15363
|
settings: z.object({
|
|
14228
|
-
|
|
15364
|
+
// The protection level (levels.ts in the engine); `observe` is Audit's older name.
|
|
15365
|
+
mode: z.enum(["observe", "audit", "soft", "standard", "strict", "block-all"]).optional(),
|
|
14229
15366
|
autoStartDaemon: z.boolean().optional(),
|
|
14230
15367
|
// enableUndo / policy.snapshot: removed with the undo feature. The
|
|
14231
15368
|
// schema strips undeclared keys silently (plain zod object — no
|
|
@@ -14354,8 +15491,8 @@ var ConfigFileSchema = z.object({
|
|
|
14354
15491
|
}).strict();
|
|
14355
15492
|
function formatIssues(issues) {
|
|
14356
15493
|
const lines = issues.map((issue) => {
|
|
14357
|
-
const
|
|
14358
|
-
return ` \u2022 ${
|
|
15494
|
+
const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
15495
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
14359
15496
|
});
|
|
14360
15497
|
return `Invalid config:
|
|
14361
15498
|
${lines.join("\n")}`;
|
|
@@ -14368,9 +15505,9 @@ function prunePaths(root, paths) {
|
|
|
14368
15505
|
const yi = y[y.length - 1];
|
|
14369
15506
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
14370
15507
|
});
|
|
14371
|
-
for (const
|
|
15508
|
+
for (const path28 of ordered) {
|
|
14372
15509
|
let cur = root;
|
|
14373
|
-
for (const key of
|
|
15510
|
+
for (const key of path28.slice(0, -1)) {
|
|
14374
15511
|
if (cur === null || typeof cur !== "object") {
|
|
14375
15512
|
cur = void 0;
|
|
14376
15513
|
break;
|
|
@@ -14378,7 +15515,7 @@ function prunePaths(root, paths) {
|
|
|
14378
15515
|
cur = cur[key];
|
|
14379
15516
|
}
|
|
14380
15517
|
if (cur === null || typeof cur !== "object") continue;
|
|
14381
|
-
const last =
|
|
15518
|
+
const last = path28[path28.length - 1];
|
|
14382
15519
|
if (Array.isArray(cur)) {
|
|
14383
15520
|
const i = Number(last);
|
|
14384
15521
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -14414,7 +15551,7 @@ function sanitizeConfig(raw) {
|
|
|
14414
15551
|
return issue.keys.map((k) => [...at, k]);
|
|
14415
15552
|
}
|
|
14416
15553
|
return [at.slice(0, -level || void 0)];
|
|
14417
|
-
}).filter((
|
|
15554
|
+
}).filter((path28) => path28.length > 0);
|
|
14418
15555
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
14419
15556
|
}
|
|
14420
15557
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -14440,6 +15577,85 @@ import fs2 from "fs";
|
|
|
14440
15577
|
import path2 from "path";
|
|
14441
15578
|
import os2 from "os";
|
|
14442
15579
|
init_dist();
|
|
15580
|
+
|
|
15581
|
+
// src/shields/build.ts
|
|
15582
|
+
init_dist();
|
|
15583
|
+
function escapeRegex(s) {
|
|
15584
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15585
|
+
}
|
|
15586
|
+
function slug(s) {
|
|
15587
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
15588
|
+
}
|
|
15589
|
+
var B = "[\\s/\\\\]";
|
|
15590
|
+
var SEP = "[/\\\\]";
|
|
15591
|
+
function pathToRegexFragment(rawPath) {
|
|
15592
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
15593
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
15594
|
+
if (segments2.length === 0) return "";
|
|
15595
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
15596
|
+
}
|
|
15597
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
15598
|
+
const value = pathToRegexFragment(rawPath);
|
|
15599
|
+
if (!value || !candidate) return false;
|
|
15600
|
+
const re = getCompiledRegex(value);
|
|
15601
|
+
if (!re) return false;
|
|
15602
|
+
return re.test(candidate);
|
|
15603
|
+
}
|
|
15604
|
+
function pathRules(rawPath, verdict, reason) {
|
|
15605
|
+
const value = pathToRegexFragment(rawPath);
|
|
15606
|
+
if (!value) return [];
|
|
15607
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
15608
|
+
const s = slug(rawPath);
|
|
15609
|
+
return [
|
|
15610
|
+
// `reading: 'access'`: a script's interpreter line and the program a
|
|
15611
|
+
// command runs are not reads of the jailed path (node9Firewall#385).
|
|
15612
|
+
{
|
|
15613
|
+
name: `${verdict}-path-${s}-bash`,
|
|
15614
|
+
tool: "bash",
|
|
15615
|
+
conditions: [{ field: "command", op: "matches", value, reading: "access" }],
|
|
15616
|
+
verdict,
|
|
15617
|
+
reason: why
|
|
15618
|
+
},
|
|
15619
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
15620
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
15621
|
+
{
|
|
15622
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
15623
|
+
tool: "*",
|
|
15624
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
15625
|
+
verdict,
|
|
15626
|
+
reason: why
|
|
15627
|
+
},
|
|
15628
|
+
{
|
|
15629
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
15630
|
+
tool: "*",
|
|
15631
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
15632
|
+
verdict,
|
|
15633
|
+
reason: why
|
|
15634
|
+
},
|
|
15635
|
+
{
|
|
15636
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
15637
|
+
tool: "*",
|
|
15638
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
15639
|
+
verdict,
|
|
15640
|
+
reason: why
|
|
15641
|
+
}
|
|
15642
|
+
];
|
|
15643
|
+
}
|
|
15644
|
+
var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
|
|
15645
|
+
function withAccessReading(def) {
|
|
15646
|
+
let changed = false;
|
|
15647
|
+
const smartRules = def.smartRules.map((r) => {
|
|
15648
|
+
const [c, ...rest] = r.conditions ?? [];
|
|
15649
|
+
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) {
|
|
15650
|
+
return r;
|
|
15651
|
+
}
|
|
15652
|
+
changed = true;
|
|
15653
|
+
return { ...r, conditions: [{ ...c, reading: "access" }] };
|
|
15654
|
+
});
|
|
15655
|
+
return changed ? { ...def, smartRules } : def;
|
|
15656
|
+
}
|
|
15657
|
+
|
|
15658
|
+
// src/shields.ts
|
|
14443
15659
|
init_dist();
|
|
14444
15660
|
var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
14445
15661
|
function loadUserShields() {
|
|
@@ -14466,7 +15682,7 @@ function loadUserShields() {
|
|
|
14466
15682
|
`);
|
|
14467
15683
|
continue;
|
|
14468
15684
|
}
|
|
14469
|
-
result[v.ok.name] = v.ok;
|
|
15685
|
+
result[v.ok.name] = withAccessReading(v.ok);
|
|
14470
15686
|
} catch (err) {
|
|
14471
15687
|
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
14472
15688
|
`);
|
|
@@ -14521,7 +15737,7 @@ function readShieldOverrides() {
|
|
|
14521
15737
|
}
|
|
14522
15738
|
|
|
14523
15739
|
// src/config/managed.ts
|
|
14524
|
-
var MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
15740
|
+
var MODE_ORDER = ["observe", "audit", "soft", "standard", "strict", "block-all"];
|
|
14525
15741
|
var EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
14526
15742
|
function rankIn(order, value) {
|
|
14527
15743
|
return value === void 0 ? -1 : order.indexOf(value);
|
|
@@ -14634,74 +15850,12 @@ function applyManagedCommandChecks(local, managed, locked) {
|
|
|
14634
15850
|
}
|
|
14635
15851
|
function applyManagedApprovers(local, managed) {
|
|
14636
15852
|
return {
|
|
14637
|
-
...local,
|
|
14638
|
-
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
14639
|
-
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
14640
|
-
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
14641
|
-
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
14642
|
-
};
|
|
14643
|
-
}
|
|
14644
|
-
|
|
14645
|
-
// src/shields/build.ts
|
|
14646
|
-
init_dist();
|
|
14647
|
-
function escapeRegex(s) {
|
|
14648
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14649
|
-
}
|
|
14650
|
-
function slug(s) {
|
|
14651
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
14652
|
-
}
|
|
14653
|
-
var B = "[\\s/\\\\]";
|
|
14654
|
-
var SEP = "[/\\\\]";
|
|
14655
|
-
function pathToRegexFragment(rawPath) {
|
|
14656
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
14657
|
-
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
14658
|
-
if (segments2.length === 0) return "";
|
|
14659
|
-
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
14660
|
-
}
|
|
14661
|
-
function pathMatchesFragment(candidate, rawPath) {
|
|
14662
|
-
const value = pathToRegexFragment(rawPath);
|
|
14663
|
-
if (!value || !candidate) return false;
|
|
14664
|
-
const re = getCompiledRegex(value);
|
|
14665
|
-
if (!re) return false;
|
|
14666
|
-
return re.test(candidate);
|
|
14667
|
-
}
|
|
14668
|
-
function pathRules(rawPath, verdict, reason) {
|
|
14669
|
-
const value = pathToRegexFragment(rawPath);
|
|
14670
|
-
if (!value) return [];
|
|
14671
|
-
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
14672
|
-
const s = slug(rawPath);
|
|
14673
|
-
return [
|
|
14674
|
-
{
|
|
14675
|
-
name: `${verdict}-path-${s}-bash`,
|
|
14676
|
-
tool: "bash",
|
|
14677
|
-
conditions: [{ field: "command", op: "matches", value }],
|
|
14678
|
-
verdict,
|
|
14679
|
-
reason: why
|
|
14680
|
-
},
|
|
14681
|
-
// Keep the historical `-anytool` name for the file_path rule: the
|
|
14682
|
-
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
14683
|
-
{
|
|
14684
|
-
name: `${verdict}-path-${s}-anytool`,
|
|
14685
|
-
tool: "*",
|
|
14686
|
-
conditions: [{ field: "file_path", op: "matches", value }],
|
|
14687
|
-
verdict,
|
|
14688
|
-
reason: why
|
|
14689
|
-
},
|
|
14690
|
-
{
|
|
14691
|
-
name: `${verdict}-path-${s}-anytool-path`,
|
|
14692
|
-
tool: "*",
|
|
14693
|
-
conditions: [{ field: "path", op: "matches", value }],
|
|
14694
|
-
verdict,
|
|
14695
|
-
reason: why
|
|
14696
|
-
},
|
|
14697
|
-
{
|
|
14698
|
-
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
14699
|
-
tool: "*",
|
|
14700
|
-
conditions: [{ field: "pattern", op: "matches", value }],
|
|
14701
|
-
verdict,
|
|
14702
|
-
reason: why
|
|
14703
|
-
}
|
|
14704
|
-
];
|
|
15853
|
+
...local,
|
|
15854
|
+
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
15855
|
+
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
15856
|
+
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
15857
|
+
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
15858
|
+
};
|
|
14705
15859
|
}
|
|
14706
15860
|
|
|
14707
15861
|
// src/auth/trusted-hosts.ts
|
|
@@ -14762,6 +15916,7 @@ function isTrustedHost(host2) {
|
|
|
14762
15916
|
|
|
14763
15917
|
// src/config/index.ts
|
|
14764
15918
|
init_dist();
|
|
15919
|
+
init_dist();
|
|
14765
15920
|
|
|
14766
15921
|
// src/config/v2.ts
|
|
14767
15922
|
init_dist();
|
|
@@ -14845,7 +16000,7 @@ function v2ToLegacy(raw, options = {}) {
|
|
|
14845
16000
|
continue;
|
|
14846
16001
|
}
|
|
14847
16002
|
if (id === "commands.unknown") {
|
|
14848
|
-
warnings.push(`checks.${id}: set "mode": "
|
|
16003
|
+
warnings.push(`checks.${id}: set "mode": "block-all" instead`);
|
|
14849
16004
|
continue;
|
|
14850
16005
|
}
|
|
14851
16006
|
const allowed = configurableValues(id);
|
|
@@ -15074,7 +16229,9 @@ var DEFAULT_CONFIG = {
|
|
|
15074
16229
|
op: "matches",
|
|
15075
16230
|
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
15076
16231
|
// fails the engine's ReDoS check and the rule would be dropped.
|
|
15077
|
-
|
|
16232
|
+
// `apps`: turning an app off, allowing one of its actions, or
|
|
16233
|
+
// linking a server to an app (the general checks leave it).
|
|
16234
|
+
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",
|
|
15078
16235
|
flags: "i"
|
|
15079
16236
|
}
|
|
15080
16237
|
],
|
|
@@ -15160,7 +16317,12 @@ var DEFAULT_CONFIG = {
|
|
|
15160
16317
|
verdict: "block",
|
|
15161
16318
|
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
15162
16319
|
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."
|
|
15163
|
-
}
|
|
16320
|
+
},
|
|
16321
|
+
// ── Actions in apps (node9Firewall#390) ───────────────────────────────
|
|
16322
|
+
// actions.money / actions.access / actions.publish: the action templates
|
|
16323
|
+
// on every MCP server no app shield claims, plus the shell publishing
|
|
16324
|
+
// rules. Review by default; the check value governs them.
|
|
16325
|
+
...GENERAL_ACTION_RULES
|
|
15164
16326
|
],
|
|
15165
16327
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
|
|
15166
16328
|
egress: {
|
|
@@ -15197,6 +16359,7 @@ var DEFAULT_CONFIG = {
|
|
|
15197
16359
|
trustedHosts: [],
|
|
15198
16360
|
trustedHostsManaged: false,
|
|
15199
16361
|
appPermissions: {},
|
|
16362
|
+
appLinks: {},
|
|
15200
16363
|
managedJailPaths: []
|
|
15201
16364
|
},
|
|
15202
16365
|
environments: {}
|
|
@@ -15376,6 +16539,11 @@ var KNOB_TO_CHECK = {
|
|
|
15376
16539
|
evalDynamic: "commands.eval-dynamic",
|
|
15377
16540
|
pipeChainHigh: "data.pipe-chain"
|
|
15378
16541
|
};
|
|
16542
|
+
function statedSignalFields(policy) {
|
|
16543
|
+
return Object.keys(tuningFromLegacyPolicy(policy)["loading.malicious-package"] ?? {}).filter(
|
|
16544
|
+
(k) => k === "newPackage" || k === "installScript"
|
|
16545
|
+
);
|
|
16546
|
+
}
|
|
15379
16547
|
function catalogSettingsFrom(settings, policy) {
|
|
15380
16548
|
return catalogSettingsFromConfig(settings, policy);
|
|
15381
16549
|
}
|
|
@@ -15431,6 +16599,7 @@ function getConfig(cwd) {
|
|
|
15431
16599
|
trustedHosts: [],
|
|
15432
16600
|
trustedHostsManaged: false,
|
|
15433
16601
|
appPermissions: {},
|
|
16602
|
+
appLinks: {},
|
|
15434
16603
|
managedJailPaths: []
|
|
15435
16604
|
};
|
|
15436
16605
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
@@ -15442,6 +16611,8 @@ function getConfig(cwd) {
|
|
|
15442
16611
|
const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
|
|
15443
16612
|
let ssrfStrictSource = "default";
|
|
15444
16613
|
const workspaceChecks = {};
|
|
16614
|
+
const managedStatedChecks = /* @__PURE__ */ new Set();
|
|
16615
|
+
const managedStatedSignals = /* @__PURE__ */ new Set();
|
|
15445
16616
|
const applyLayer = (source, isProject = false, isCloud = false) => {
|
|
15446
16617
|
if (!source) return;
|
|
15447
16618
|
const s = source.settings || {};
|
|
@@ -15649,9 +16820,11 @@ function getConfig(cwd) {
|
|
|
15649
16820
|
if (raw.managedConfig && typeof raw.managedConfig === "object") {
|
|
15650
16821
|
const mc = raw.managedConfig;
|
|
15651
16822
|
const locked = Array.isArray(mc.locked) ? mc.locked.filter((f) => typeof f === "string") : [];
|
|
16823
|
+
for (const id of knobStatedChecks(mc)) managedStatedChecks.add(id);
|
|
16824
|
+
for (const k of statedSignalFields(mc)) managedStatedSignals.add(k);
|
|
15652
16825
|
if (typeof mc.mode === "string") {
|
|
15653
16826
|
if (keyed) {
|
|
15654
|
-
if (
|
|
16827
|
+
if (isModeValue(mc.mode)) {
|
|
15655
16828
|
mergedSettings.mode = mc.mode;
|
|
15656
16829
|
}
|
|
15657
16830
|
} else {
|
|
@@ -15815,19 +16988,26 @@ function getConfig(cwd) {
|
|
|
15815
16988
|
}
|
|
15816
16989
|
if (Array.isArray(mc.jailPaths)) {
|
|
15817
16990
|
for (const jp of mc.jailPaths) {
|
|
15818
|
-
const
|
|
15819
|
-
if (!
|
|
16991
|
+
const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
16992
|
+
if (!path28) continue;
|
|
15820
16993
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
15821
|
-
for (const r of pathRules(
|
|
16994
|
+
for (const r of pathRules(path28, verdict, "org-managed jail")) {
|
|
15822
16995
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
15823
16996
|
}
|
|
15824
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
16997
|
+
mergedPolicy.managedJailPaths.push({ path: path28, verdict });
|
|
15825
16998
|
}
|
|
15826
16999
|
}
|
|
15827
17000
|
if (Array.isArray(mc.trustedHosts)) {
|
|
15828
17001
|
mergedPolicy.trustedHostsManaged = true;
|
|
15829
17002
|
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
15830
17003
|
}
|
|
17004
|
+
if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
|
|
17005
|
+
const links = {};
|
|
17006
|
+
for (const [key, slug2] of Object.entries(mc.appLinks)) {
|
|
17007
|
+
if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
|
|
17008
|
+
}
|
|
17009
|
+
mergedPolicy.appLinks = links;
|
|
17010
|
+
}
|
|
15831
17011
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
15832
17012
|
const coerced = {};
|
|
15833
17013
|
for (const [srv, tools] of Object.entries(mc.appPermissions)) {
|
|
@@ -15926,14 +17106,15 @@ function getConfig(cwd) {
|
|
|
15926
17106
|
const envMode = process.env.NODE9_MODE;
|
|
15927
17107
|
if (envMode && !modeCloudControlled && // PR-2: one listening point includes the env var — a keyed machine's
|
|
15928
17108
|
// mode comes from the workspace (or the shipped default), never the shell.
|
|
15929
|
-
!keyed &&
|
|
17109
|
+
!keyed && isModeValue(envMode)) {
|
|
15930
17110
|
mergedSettings.mode = envMode;
|
|
15931
17111
|
}
|
|
15932
|
-
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode
|
|
17112
|
+
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && isRecordOnly(mergedSettings.mode)) {
|
|
15933
17113
|
mergedSettings.mode = "standard";
|
|
15934
17114
|
}
|
|
15935
|
-
const
|
|
15936
|
-
|
|
17115
|
+
const cloudStrictLevel = levelOf(mergedSettings.mode) === "strict" || levelOf(mergedSettings.mode) === "block-all";
|
|
17116
|
+
const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && cloudStrictLevel;
|
|
17117
|
+
if (modeCloudControlled && cloudStrictLevel) {
|
|
15937
17118
|
for (const name of Object.keys(mergedEnvironments)) {
|
|
15938
17119
|
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
15939
17120
|
const cleaned = { ...mergedEnvironments[name] };
|
|
@@ -15951,6 +17132,35 @@ function getConfig(cwd) {
|
|
|
15951
17132
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
15952
17133
|
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
15953
17134
|
const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
|
|
17135
|
+
const level = levelOf(mergedSettings.mode);
|
|
17136
|
+
const levelKnobChecks = Object.entries(levelChecks(level)).filter(([id]) => isKnobGoverned(id));
|
|
17137
|
+
const signals = levelPackageSignals(level);
|
|
17138
|
+
if (levelKnobChecks.length > 0 || signals) {
|
|
17139
|
+
const policyOf = (layer) => layer?.policy ?? {};
|
|
17140
|
+
const trusted = /* @__PURE__ */ new Set([...managedStatedChecks, ...Object.keys(workspaceChecks)]);
|
|
17141
|
+
const trustedSignals = new Set(managedStatedSignals);
|
|
17142
|
+
const project = /* @__PURE__ */ new Set();
|
|
17143
|
+
const projectSignals = /* @__PURE__ */ new Set();
|
|
17144
|
+
if (!keyed) {
|
|
17145
|
+
for (const id of knobStatedChecks(policyOf(globalConfig))) trusted.add(id);
|
|
17146
|
+
for (const k of statedSignalFields(policyOf(globalConfig))) trustedSignals.add(k);
|
|
17147
|
+
for (const id of knobStatedChecks(policyOf(projectConfig))) project.add(id);
|
|
17148
|
+
for (const k of statedSignalFields(policyOf(projectConfig))) projectSignals.add(k);
|
|
17149
|
+
}
|
|
17150
|
+
const before = resolveCheckMap(catalogSettingsFromConfig({}, mergedPolicy));
|
|
17151
|
+
for (const [id, v] of levelKnobChecks) {
|
|
17152
|
+
if (trusted.has(id)) continue;
|
|
17153
|
+
if (project.has(id) && CHECK_VERDICT_RANK[v] <= CHECK_VERDICT_RANK[before[id] ?? "off"])
|
|
17154
|
+
continue;
|
|
17155
|
+
checkToLegacyPolicy(mergedPolicy, id, v);
|
|
17156
|
+
}
|
|
17157
|
+
if (signals) {
|
|
17158
|
+
for (const key of ["newPackage", "installScript"]) {
|
|
17159
|
+
if (trustedSignals.has(key) || projectSignals.has(key)) continue;
|
|
17160
|
+
mergedPolicy.packageCheck[key] = signals[key];
|
|
17161
|
+
}
|
|
17162
|
+
}
|
|
17163
|
+
}
|
|
15954
17164
|
const explicit = {};
|
|
15955
17165
|
const checkSources = {};
|
|
15956
17166
|
if (keyed) {
|
|
@@ -16122,6 +17332,9 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
|
|
|
16122
17332
|
// src/utils/regex.ts
|
|
16123
17333
|
init_dist();
|
|
16124
17334
|
|
|
17335
|
+
// src/policy/index.ts
|
|
17336
|
+
init_dist();
|
|
17337
|
+
|
|
16125
17338
|
// src/dlp.ts
|
|
16126
17339
|
init_dist();
|
|
16127
17340
|
init_dist();
|
|
@@ -16373,7 +17586,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
16373
17586
|
cwd: prepared.cwd,
|
|
16374
17587
|
activeEnvironment,
|
|
16375
17588
|
skipIgnoredFastPath: opts?.skipIgnoredFastPath,
|
|
16376
|
-
argumentScan: opts?.argumentScan
|
|
17589
|
+
argumentScan: opts?.argumentScan,
|
|
17590
|
+
mcpServer: opts?.mcpServer
|
|
16377
17591
|
},
|
|
16378
17592
|
{
|
|
16379
17593
|
checkProvenance,
|
|
@@ -17041,14 +18255,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
|
17041
18255
|
|
|
17042
18256
|
// src/auth/orchestrator.ts
|
|
17043
18257
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
17044
|
-
|
|
18258
|
+
|
|
18259
|
+
// src/apps/state.ts
|
|
18260
|
+
import fs12 from "fs";
|
|
18261
|
+
import os10 from "os";
|
|
18262
|
+
import path12 from "path";
|
|
18263
|
+
|
|
18264
|
+
// src/apps/catalog.ts
|
|
18265
|
+
var APP_CATALOG = [
|
|
18266
|
+
{
|
|
18267
|
+
slug: "redis",
|
|
18268
|
+
label: "Redis",
|
|
18269
|
+
shield: "redis",
|
|
18270
|
+
servers: ["redis", "valkey"],
|
|
18271
|
+
commands: ["redis-cli", "valkey-cli"],
|
|
18272
|
+
packages: ["redis", "ioredis", "valkey"],
|
|
18273
|
+
envPrefixes: ["REDIS_"]
|
|
18274
|
+
},
|
|
18275
|
+
{
|
|
18276
|
+
slug: "postgres",
|
|
18277
|
+
label: "PostgreSQL",
|
|
18278
|
+
shield: "postgres",
|
|
18279
|
+
servers: ["postgres", "pgsql"],
|
|
18280
|
+
commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
|
|
18281
|
+
packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
|
|
18282
|
+
envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
|
|
18283
|
+
},
|
|
18284
|
+
{
|
|
18285
|
+
slug: "mongodb",
|
|
18286
|
+
label: "MongoDB",
|
|
18287
|
+
shield: "mongodb",
|
|
18288
|
+
servers: ["mongo"],
|
|
18289
|
+
commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
|
|
18290
|
+
packages: ["mongodb", "mongoose", "pymongo", "motor"],
|
|
18291
|
+
envPrefixes: ["MONGO"]
|
|
18292
|
+
},
|
|
18293
|
+
{
|
|
18294
|
+
slug: "github",
|
|
18295
|
+
label: "GitHub",
|
|
18296
|
+
shield: "github",
|
|
18297
|
+
servers: ["github"],
|
|
18298
|
+
commands: ["gh"],
|
|
18299
|
+
packages: ["@octokit/", "octokit", "pygithub"],
|
|
18300
|
+
envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
|
|
18301
|
+
},
|
|
18302
|
+
{
|
|
18303
|
+
slug: "aws",
|
|
18304
|
+
label: "AWS",
|
|
18305
|
+
shield: "aws",
|
|
18306
|
+
servers: ["aws"],
|
|
18307
|
+
commands: ["aws", "sam", "cdk"],
|
|
18308
|
+
packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
|
|
18309
|
+
envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
|
|
18310
|
+
},
|
|
18311
|
+
{
|
|
18312
|
+
slug: "k8s",
|
|
18313
|
+
label: "Kubernetes",
|
|
18314
|
+
shield: "k8s",
|
|
18315
|
+
servers: ["kubernetes", "k8s", "kubectl"],
|
|
18316
|
+
commands: ["kubectl", "helm", "k9s"],
|
|
18317
|
+
packages: ["@kubernetes/client-node", "kubernetes"],
|
|
18318
|
+
envPrefixes: ["KUBECONFIG"]
|
|
18319
|
+
},
|
|
18320
|
+
{
|
|
18321
|
+
slug: "docker",
|
|
18322
|
+
label: "Docker",
|
|
18323
|
+
shield: "docker",
|
|
18324
|
+
servers: ["docker"],
|
|
18325
|
+
commands: ["docker", "docker-compose", "podman"],
|
|
18326
|
+
packages: ["dockerode", "docker"],
|
|
18327
|
+
envPrefixes: ["DOCKER_HOST"]
|
|
18328
|
+
},
|
|
18329
|
+
{
|
|
18330
|
+
slug: "stripe",
|
|
18331
|
+
label: "Stripe",
|
|
18332
|
+
shield: "stripe",
|
|
18333
|
+
servers: ["stripe"],
|
|
18334
|
+
commands: ["stripe"],
|
|
18335
|
+
packages: ["stripe", "@stripe/"],
|
|
18336
|
+
envPrefixes: ["STRIPE_"]
|
|
18337
|
+
},
|
|
18338
|
+
{
|
|
18339
|
+
slug: "paypal",
|
|
18340
|
+
label: "PayPal",
|
|
18341
|
+
shield: "paypal",
|
|
18342
|
+
servers: ["paypal"],
|
|
18343
|
+
commands: [],
|
|
18344
|
+
packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
|
|
18345
|
+
envPrefixes: ["PAYPAL_"]
|
|
18346
|
+
},
|
|
18347
|
+
{
|
|
18348
|
+
slug: "square",
|
|
18349
|
+
label: "Square",
|
|
18350
|
+
shield: "square",
|
|
18351
|
+
servers: ["square"],
|
|
18352
|
+
// Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
|
|
18353
|
+
notServers: ["squarespace"],
|
|
18354
|
+
commands: [],
|
|
18355
|
+
packages: ["square", "squareup"],
|
|
18356
|
+
envPrefixes: ["SQUARE_"]
|
|
18357
|
+
},
|
|
18358
|
+
{
|
|
18359
|
+
slug: "plaid",
|
|
18360
|
+
label: "Plaid",
|
|
18361
|
+
shield: "plaid",
|
|
18362
|
+
servers: ["plaid"],
|
|
18363
|
+
commands: [],
|
|
18364
|
+
packages: ["plaid", "plaid-python"],
|
|
18365
|
+
envPrefixes: ["PLAID_"]
|
|
18366
|
+
},
|
|
18367
|
+
{
|
|
18368
|
+
slug: "okta",
|
|
18369
|
+
label: "Okta",
|
|
18370
|
+
shield: "okta",
|
|
18371
|
+
servers: ["okta"],
|
|
18372
|
+
commands: ["okta"],
|
|
18373
|
+
packages: ["@okta/", "okta"],
|
|
18374
|
+
envPrefixes: ["OKTA_"]
|
|
18375
|
+
},
|
|
18376
|
+
{
|
|
18377
|
+
slug: "auth0",
|
|
18378
|
+
label: "Auth0",
|
|
18379
|
+
shield: "auth0",
|
|
18380
|
+
servers: ["auth0"],
|
|
18381
|
+
commands: ["auth0"],
|
|
18382
|
+
packages: ["auth0", "@auth0/"],
|
|
18383
|
+
envPrefixes: ["AUTH0_"]
|
|
18384
|
+
}
|
|
18385
|
+
];
|
|
18386
|
+
var APP_BY_SLUG = new Map(
|
|
18387
|
+
APP_CATALOG.map((a) => [a.slug, a])
|
|
18388
|
+
);
|
|
18389
|
+
|
|
18390
|
+
// src/apps/state.ts
|
|
18391
|
+
init_audit();
|
|
18392
|
+
|
|
18393
|
+
// src/daemon/mcp-tools.ts
|
|
18394
|
+
import fs11 from "fs";
|
|
18395
|
+
import path11 from "path";
|
|
18396
|
+
import os9 from "os";
|
|
18397
|
+
function getMcpToolsFile() {
|
|
18398
|
+
return path11.join(os9.homedir(), ".node9", "mcp-tools.json");
|
|
18399
|
+
}
|
|
18400
|
+
function readMcpToolsConfig() {
|
|
18401
|
+
try {
|
|
18402
|
+
const file2 = getMcpToolsFile();
|
|
18403
|
+
if (!fs11.existsSync(file2)) return {};
|
|
18404
|
+
const raw = fs11.readFileSync(file2, "utf-8");
|
|
18405
|
+
return JSON.parse(raw);
|
|
18406
|
+
} catch {
|
|
18407
|
+
return {};
|
|
18408
|
+
}
|
|
18409
|
+
}
|
|
18410
|
+
|
|
18411
|
+
// src/apps/state.ts
|
|
18412
|
+
function appsFilePath(home = os10.homedir()) {
|
|
18413
|
+
return path12.join(home, ".node9", "apps.json");
|
|
18414
|
+
}
|
|
18415
|
+
var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
|
|
18416
|
+
function emptyFile() {
|
|
18417
|
+
return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
|
|
18418
|
+
}
|
|
18419
|
+
function ownString(map, key) {
|
|
18420
|
+
if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
|
|
18421
|
+
const v = map[key];
|
|
18422
|
+
return typeof v === "string" ? v : void 0;
|
|
18423
|
+
}
|
|
18424
|
+
function readSignals(raw) {
|
|
18425
|
+
if (!Array.isArray(raw)) return [];
|
|
18426
|
+
const out = [];
|
|
18427
|
+
for (const x of raw) {
|
|
18428
|
+
if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
|
|
18429
|
+
if (!SIGNAL_KINDS.has(x.kind)) continue;
|
|
18430
|
+
out.push({
|
|
18431
|
+
kind: x.kind,
|
|
18432
|
+
value: x.value.slice(0, 256),
|
|
18433
|
+
...typeof x.project === "string" ? { project: x.project } : {}
|
|
18434
|
+
});
|
|
18435
|
+
}
|
|
18436
|
+
return out;
|
|
18437
|
+
}
|
|
18438
|
+
function parseAppsFile(text) {
|
|
18439
|
+
const raw = JSON.parse(text);
|
|
18440
|
+
const file2 = emptyFile();
|
|
18441
|
+
for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
|
|
18442
|
+
if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
|
|
18443
|
+
file2.apps[slug2] = {
|
|
18444
|
+
state: r.state,
|
|
18445
|
+
by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
|
|
18446
|
+
at: typeof r.at === "string" ? r.at : "",
|
|
18447
|
+
signals: readSignals(r.signals)
|
|
18448
|
+
};
|
|
18449
|
+
}
|
|
18450
|
+
for (const [server, slug2] of Object.entries(raw.links ?? {})) {
|
|
18451
|
+
if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
|
|
18452
|
+
}
|
|
18453
|
+
return file2;
|
|
18454
|
+
}
|
|
18455
|
+
function readAppsFile(home) {
|
|
18456
|
+
return readAppsFileStrict(home) ?? emptyFile();
|
|
18457
|
+
}
|
|
18458
|
+
function readAppsFileStrict(home) {
|
|
18459
|
+
let text;
|
|
18460
|
+
try {
|
|
18461
|
+
text = fs12.readFileSync(appsFilePath(home), "utf8");
|
|
18462
|
+
} catch (e) {
|
|
18463
|
+
return e.code === "ENOENT" ? emptyFile() : null;
|
|
18464
|
+
}
|
|
18465
|
+
try {
|
|
18466
|
+
return parseAppsFile(text);
|
|
18467
|
+
} catch {
|
|
18468
|
+
return null;
|
|
18469
|
+
}
|
|
18470
|
+
}
|
|
18471
|
+
var linkCache = null;
|
|
18472
|
+
var NO_LINKS = /* @__PURE__ */ Object.create(null);
|
|
18473
|
+
function localLinks() {
|
|
18474
|
+
const file2 = appsFilePath();
|
|
18475
|
+
try {
|
|
18476
|
+
const mtimeMs = fs12.statSync(file2).mtimeMs;
|
|
18477
|
+
if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
|
|
18478
|
+
linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
|
|
18479
|
+
}
|
|
18480
|
+
return linkCache.links;
|
|
18481
|
+
} catch {
|
|
18482
|
+
return NO_LINKS;
|
|
18483
|
+
}
|
|
18484
|
+
}
|
|
18485
|
+
function resolveLinkedServer(label2, opts = {}) {
|
|
18486
|
+
if (typeof label2 !== "string" || !label2) return label2;
|
|
18487
|
+
const applied = new Set(opts.appliedShields ?? []);
|
|
18488
|
+
const live = (slug2) => {
|
|
18489
|
+
const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
|
|
18490
|
+
return app && applied.has(app.shield) ? slug2 : void 0;
|
|
18491
|
+
};
|
|
18492
|
+
const lower2 = label2.toLowerCase();
|
|
18493
|
+
if (!opts.workspaceGoverned) {
|
|
18494
|
+
const local = live(ownString(localLinks(), lower2));
|
|
18495
|
+
if (local) return local;
|
|
18496
|
+
}
|
|
18497
|
+
const managed = opts.managedLinks ?? {};
|
|
18498
|
+
if (opts.serverKey) {
|
|
18499
|
+
const byKey = live(ownString(managed, opts.serverKey));
|
|
18500
|
+
if (byKey) return byKey;
|
|
18501
|
+
}
|
|
18502
|
+
const keys = Object.keys(managed);
|
|
18503
|
+
if (keys.length === 0) return label2;
|
|
18504
|
+
const tools = readMcpToolsConfig();
|
|
18505
|
+
for (const key of keys) {
|
|
18506
|
+
const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
|
|
18507
|
+
if (typeof name === "string" && name.toLowerCase() === lower2) {
|
|
18508
|
+
const linked = live(ownString(managed, key));
|
|
18509
|
+
if (linked) return linked;
|
|
18510
|
+
}
|
|
18511
|
+
}
|
|
18512
|
+
return label2;
|
|
18513
|
+
}
|
|
18514
|
+
|
|
18515
|
+
// src/auth/orchestrator.ts
|
|
18516
|
+
import os20 from "os";
|
|
17045
18517
|
|
|
17046
18518
|
// src/ui/native.ts
|
|
17047
18519
|
import { spawn } from "child_process";
|
|
17048
|
-
import
|
|
18520
|
+
import path14 from "path";
|
|
17049
18521
|
|
|
17050
18522
|
// src/context-sniper.ts
|
|
17051
|
-
import
|
|
18523
|
+
import path13 from "path";
|
|
17052
18524
|
function smartTruncate(str, maxLen = 500) {
|
|
17053
18525
|
if (str.length <= maxLen) return str;
|
|
17054
18526
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -17116,7 +18588,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
17116
18588
|
intent = "EDIT";
|
|
17117
18589
|
if (obj.file_path) {
|
|
17118
18590
|
editFilePath = String(obj.file_path);
|
|
17119
|
-
editFileName =
|
|
18591
|
+
editFileName = path13.basename(editFilePath);
|
|
17120
18592
|
}
|
|
17121
18593
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
17122
18594
|
contextSnippet = result.snippet;
|
|
@@ -17184,7 +18656,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
17184
18656
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17185
18657
|
const obj = parsed;
|
|
17186
18658
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
17187
|
-
const file2 = obj.file_path ?
|
|
18659
|
+
const file2 = obj.file_path ? path14.basename(String(obj.file_path)) : "file";
|
|
17188
18660
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
17189
18661
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
17190
18662
|
return {
|
|
@@ -17387,24 +18859,24 @@ end run`;
|
|
|
17387
18859
|
}
|
|
17388
18860
|
|
|
17389
18861
|
// src/canary/registry.ts
|
|
17390
|
-
import
|
|
17391
|
-
import
|
|
17392
|
-
import
|
|
18862
|
+
import fs14 from "fs";
|
|
18863
|
+
import os12 from "os";
|
|
18864
|
+
import path16 from "path";
|
|
17393
18865
|
init_dist();
|
|
17394
18866
|
|
|
17395
18867
|
// src/shields/jail.ts
|
|
17396
|
-
import
|
|
17397
|
-
import
|
|
17398
|
-
import
|
|
18868
|
+
import fs13 from "fs";
|
|
18869
|
+
import os11 from "os";
|
|
18870
|
+
import path15 from "path";
|
|
17399
18871
|
init_dist();
|
|
17400
18872
|
var USER_JAIL_SHIELD = "user-jail";
|
|
17401
18873
|
function jailStorePath() {
|
|
17402
|
-
return
|
|
18874
|
+
return path15.join(os11.homedir(), ".node9", "jail-paths.json");
|
|
17403
18875
|
}
|
|
17404
18876
|
function readJailPaths() {
|
|
17405
18877
|
let text;
|
|
17406
18878
|
try {
|
|
17407
|
-
text =
|
|
18879
|
+
text = fs13.readFileSync(jailStorePath(), "utf8");
|
|
17408
18880
|
} catch (err) {
|
|
17409
18881
|
if (err.code === "ENOENT") return [];
|
|
17410
18882
|
throw err;
|
|
@@ -17433,7 +18905,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
17433
18905
|
|
|
17434
18906
|
// src/canary/registry.ts
|
|
17435
18907
|
function canaryStorePath() {
|
|
17436
|
-
return
|
|
18908
|
+
return path16.join(os12.homedir(), ".node9", "canaries.json");
|
|
17437
18909
|
}
|
|
17438
18910
|
function isRecord(x) {
|
|
17439
18911
|
if (!x || typeof x !== "object") return false;
|
|
@@ -17444,7 +18916,7 @@ function loadCanaries(opts) {
|
|
|
17444
18916
|
const p = canaryStorePath();
|
|
17445
18917
|
let raw;
|
|
17446
18918
|
try {
|
|
17447
|
-
raw =
|
|
18919
|
+
raw = fs14.readFileSync(p, "utf-8");
|
|
17448
18920
|
} catch (e) {
|
|
17449
18921
|
if (e.code === "ENOENT") return [];
|
|
17450
18922
|
throw e;
|
|
@@ -17471,21 +18943,21 @@ init_dist();
|
|
|
17471
18943
|
|
|
17472
18944
|
// src/auth/session-script.ts
|
|
17473
18945
|
init_dist();
|
|
17474
|
-
import
|
|
17475
|
-
import
|
|
17476
|
-
import
|
|
18946
|
+
import fs15 from "fs";
|
|
18947
|
+
import os13 from "os";
|
|
18948
|
+
import path17 from "path";
|
|
17477
18949
|
var MAX_SCRIPT_BYTES = 64 * 1024;
|
|
17478
18950
|
var MAX_RECORDED_WRITES = 32;
|
|
17479
18951
|
function readScriptFile(resolved) {
|
|
17480
18952
|
let fd;
|
|
17481
18953
|
try {
|
|
17482
|
-
fd =
|
|
17483
|
-
const st =
|
|
18954
|
+
fd = fs15.openSync(resolved, fs15.constants.O_RDONLY | (fs15.constants.O_NONBLOCK ?? 0));
|
|
18955
|
+
const st = fs15.fstatSync(fd);
|
|
17484
18956
|
if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
|
|
17485
18957
|
const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
|
|
17486
18958
|
let n = 0;
|
|
17487
18959
|
while (n < buf.length) {
|
|
17488
|
-
const got =
|
|
18960
|
+
const got = fs15.readSync(fd, buf, n, buf.length - n, n);
|
|
17489
18961
|
if (got === 0) break;
|
|
17490
18962
|
n += got;
|
|
17491
18963
|
}
|
|
@@ -17496,7 +18968,7 @@ function readScriptFile(resolved) {
|
|
|
17496
18968
|
} finally {
|
|
17497
18969
|
if (fd !== void 0) {
|
|
17498
18970
|
try {
|
|
17499
|
-
|
|
18971
|
+
fs15.closeSync(fd);
|
|
17500
18972
|
} catch {
|
|
17501
18973
|
}
|
|
17502
18974
|
}
|
|
@@ -17504,9 +18976,9 @@ function readScriptFile(resolved) {
|
|
|
17504
18976
|
}
|
|
17505
18977
|
function absolute(p, cwd, home) {
|
|
17506
18978
|
const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
|
|
17507
|
-
return
|
|
18979
|
+
return path17.resolve(cwd, spelled);
|
|
17508
18980
|
}
|
|
17509
|
-
async function resolveSessionScript(toolName, args, cwd, layers, deps, home =
|
|
18981
|
+
async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os13.homedir()) {
|
|
17510
18982
|
try {
|
|
17511
18983
|
const d = deps ?? { checkWritten, readScript: readScriptFile };
|
|
17512
18984
|
const shell = resolveShellInput(toolName, args, layers);
|
|
@@ -17535,7 +19007,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os
|
|
|
17535
19007
|
return null;
|
|
17536
19008
|
}
|
|
17537
19009
|
}
|
|
17538
|
-
function sessionWriteTargets(toolName, args, fileAccess, cwd, home =
|
|
19010
|
+
function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os13.homedir()) {
|
|
17539
19011
|
const base2 = cwd ?? process.cwd();
|
|
17540
19012
|
const out = /* @__PURE__ */ new Set();
|
|
17541
19013
|
const argsObj = args && typeof args === "object" ? args : {};
|
|
@@ -17565,26 +19037,26 @@ init_audit();
|
|
|
17565
19037
|
init_dist();
|
|
17566
19038
|
|
|
17567
19039
|
// src/exposure/known-hosts.ts
|
|
17568
|
-
import
|
|
17569
|
-
import
|
|
19040
|
+
import fs17 from "fs";
|
|
19041
|
+
import path19 from "path";
|
|
17570
19042
|
|
|
17571
19043
|
// src/utils/git-remotes.ts
|
|
17572
19044
|
init_dist();
|
|
17573
|
-
import
|
|
17574
|
-
import
|
|
19045
|
+
import fs16 from "fs";
|
|
19046
|
+
import path18 from "path";
|
|
17575
19047
|
function readSmallFile(p, max) {
|
|
17576
19048
|
let fd;
|
|
17577
19049
|
try {
|
|
17578
|
-
fd =
|
|
17579
|
-
const st =
|
|
19050
|
+
fd = fs16.openSync(p, fs16.constants.O_RDONLY | (fs16.constants.O_NOFOLLOW ?? 0));
|
|
19051
|
+
const st = fs16.fstatSync(fd);
|
|
17580
19052
|
if (!st.isFile() || st.size > max) return null;
|
|
17581
19053
|
const buf = Buffer.alloc(st.size);
|
|
17582
|
-
const n =
|
|
19054
|
+
const n = fs16.readSync(fd, buf, 0, st.size, 0);
|
|
17583
19055
|
return buf.subarray(0, n).toString("utf8");
|
|
17584
19056
|
} catch {
|
|
17585
19057
|
return null;
|
|
17586
19058
|
} finally {
|
|
17587
|
-
if (fd !== void 0)
|
|
19059
|
+
if (fd !== void 0) fs16.closeSync(fd);
|
|
17588
19060
|
}
|
|
17589
19061
|
}
|
|
17590
19062
|
function gitRemoteHost(url) {
|
|
@@ -17603,20 +19075,20 @@ function gitRemoteHost(url) {
|
|
|
17603
19075
|
return null;
|
|
17604
19076
|
}
|
|
17605
19077
|
function findGitConfig(dir2, walkUp = true) {
|
|
17606
|
-
let cur =
|
|
19078
|
+
let cur = path18.resolve(dir2);
|
|
17607
19079
|
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
17608
|
-
const dotGit =
|
|
17609
|
-
const direct = readSmallFile(
|
|
19080
|
+
const dotGit = path18.join(cur, ".git");
|
|
19081
|
+
const direct = readSmallFile(path18.join(dotGit, "config"), 1e6);
|
|
17610
19082
|
if (direct !== null) return direct;
|
|
17611
19083
|
const pointer = readSmallFile(dotGit, 4096);
|
|
17612
19084
|
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
17613
19085
|
if (m) {
|
|
17614
|
-
let gitDir =
|
|
17615
|
-
const common = readSmallFile(
|
|
17616
|
-
if (common !== null) gitDir =
|
|
17617
|
-
return readSmallFile(
|
|
19086
|
+
let gitDir = path18.resolve(cur, m[1]);
|
|
19087
|
+
const common = readSmallFile(path18.join(gitDir, "commondir"), 4096);
|
|
19088
|
+
if (common !== null) gitDir = path18.resolve(gitDir, common.trim());
|
|
19089
|
+
return readSmallFile(path18.join(gitDir, "config"), 1e6);
|
|
17618
19090
|
}
|
|
17619
|
-
const parent =
|
|
19091
|
+
const parent = path18.dirname(cur);
|
|
17620
19092
|
if (parent === cur) return null;
|
|
17621
19093
|
cur = parent;
|
|
17622
19094
|
}
|
|
@@ -17626,12 +19098,12 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
17626
19098
|
function readGitRemotes(dir2, opts = {}) {
|
|
17627
19099
|
if (!dir2) return [];
|
|
17628
19100
|
const walkUp = opts.walkUp !== false;
|
|
17629
|
-
const key = `${walkUp ? "up" : "at"}:${
|
|
19101
|
+
const key = `${walkUp ? "up" : "at"}:${path18.resolve(dir2)}`;
|
|
17630
19102
|
const hit = cache.get(key);
|
|
17631
19103
|
if (hit) return hit;
|
|
17632
19104
|
const out = [];
|
|
17633
19105
|
try {
|
|
17634
|
-
const cfg = findGitConfig(
|
|
19106
|
+
const cfg = findGitConfig(path18.resolve(dir2), walkUp);
|
|
17635
19107
|
if (cfg !== null) {
|
|
17636
19108
|
let current = null;
|
|
17637
19109
|
for (const line of cfg.split(/\r?\n/)) {
|
|
@@ -17667,15 +19139,15 @@ var ECOSYSTEMS = [
|
|
|
17667
19139
|
];
|
|
17668
19140
|
var cache2 = /* @__PURE__ */ new Map();
|
|
17669
19141
|
function manifestRoot(dir2) {
|
|
17670
|
-
let cur =
|
|
19142
|
+
let cur = path19.resolve(dir2);
|
|
17671
19143
|
for (let i = 0; i < 64; i++) {
|
|
17672
19144
|
for (const [m] of ECOSYSTEMS) {
|
|
17673
19145
|
try {
|
|
17674
|
-
if (
|
|
19146
|
+
if (fs17.existsSync(path19.join(cur, m))) return cur;
|
|
17675
19147
|
} catch {
|
|
17676
19148
|
}
|
|
17677
19149
|
}
|
|
17678
|
-
const parent =
|
|
19150
|
+
const parent = path19.dirname(cur);
|
|
17679
19151
|
if (parent === cur) return null;
|
|
17680
19152
|
cur = parent;
|
|
17681
19153
|
}
|
|
@@ -17684,7 +19156,7 @@ function manifestRoot(dir2) {
|
|
|
17684
19156
|
function knownProjectHosts(dir2) {
|
|
17685
19157
|
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
17686
19158
|
if (!dir2) return empty;
|
|
17687
|
-
const key =
|
|
19159
|
+
const key = path19.resolve(dir2);
|
|
17688
19160
|
const hit = cache2.get(key);
|
|
17689
19161
|
if (hit) return hit;
|
|
17690
19162
|
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
@@ -17697,10 +19169,10 @@ function knownProjectHosts(dir2) {
|
|
|
17697
19169
|
const root = manifestRoot(key);
|
|
17698
19170
|
if (root !== null) {
|
|
17699
19171
|
for (const [m, registry] of ECOSYSTEMS) {
|
|
17700
|
-
if (
|
|
19172
|
+
if (fs17.existsSync(path19.join(root, m))) out.hosts.add(registry);
|
|
17701
19173
|
}
|
|
17702
19174
|
try {
|
|
17703
|
-
if (
|
|
19175
|
+
if (fs17.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
17704
19176
|
out.hosts.add("host:rubygems.org");
|
|
17705
19177
|
} catch {
|
|
17706
19178
|
}
|
|
@@ -17713,9 +19185,9 @@ function knownProjectHosts(dir2) {
|
|
|
17713
19185
|
|
|
17714
19186
|
// src/exposure/writes.ts
|
|
17715
19187
|
init_dist();
|
|
17716
|
-
import
|
|
17717
|
-
import
|
|
17718
|
-
import
|
|
19188
|
+
import fs18 from "fs";
|
|
19189
|
+
import os14 from "os";
|
|
19190
|
+
import path20 from "path";
|
|
17719
19191
|
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
17720
19192
|
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
17721
19193
|
function writtenContent(args) {
|
|
@@ -17744,7 +19216,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17744
19216
|
if (typeof a.content === "string") {
|
|
17745
19217
|
try {
|
|
17746
19218
|
const next = JSON.parse(a.content);
|
|
17747
|
-
const prev = JSON.parse(
|
|
19219
|
+
const prev = JSON.parse(fs18.readFileSync(absPath, "utf8"));
|
|
17748
19220
|
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
17749
19221
|
} catch {
|
|
17750
19222
|
return LIFECYCLE_RE.test(a.content);
|
|
@@ -17754,8 +19226,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17754
19226
|
}
|
|
17755
19227
|
function judgeWritesAfterExposure(opts) {
|
|
17756
19228
|
const holds = [];
|
|
17757
|
-
const home = opts.home ??
|
|
17758
|
-
const tmp = normalizeFilePath(
|
|
19229
|
+
const home = opts.home ?? os14.homedir();
|
|
19230
|
+
const tmp = normalizeFilePath(os14.tmpdir());
|
|
17759
19231
|
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
17760
19232
|
const seen = /* @__PURE__ */ new Set();
|
|
17761
19233
|
for (const access of opts.accesses) {
|
|
@@ -17765,7 +19237,7 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17765
19237
|
seen.add(p);
|
|
17766
19238
|
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
17767
19239
|
if (root !== null && isInside(root, p)) {
|
|
17768
|
-
if (isAgentInstructionPath(p, root) && (
|
|
19240
|
+
if (isAgentInstructionPath(p, root) && (path20.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
17769
19241
|
holds.push({ path: p, why: "agent-instruction" });
|
|
17770
19242
|
continue;
|
|
17771
19243
|
}
|
|
@@ -17787,25 +19259,25 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17787
19259
|
|
|
17788
19260
|
// src/exposure/marker.ts
|
|
17789
19261
|
import crypto3 from "crypto";
|
|
17790
|
-
import
|
|
17791
|
-
import
|
|
17792
|
-
import
|
|
19262
|
+
import fs19 from "fs";
|
|
19263
|
+
import os15 from "os";
|
|
19264
|
+
import path21 from "path";
|
|
17793
19265
|
|
|
17794
19266
|
// src/daemon/exposure-store.ts
|
|
17795
19267
|
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
17796
19268
|
|
|
17797
19269
|
// src/exposure/marker.ts
|
|
17798
19270
|
function dir() {
|
|
17799
|
-
return
|
|
19271
|
+
return path21.join(os15.homedir(), ".node9", "exposure-sessions");
|
|
17800
19272
|
}
|
|
17801
19273
|
function file(sessionId) {
|
|
17802
19274
|
const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
17803
|
-
return
|
|
19275
|
+
return path21.join(dir(), id);
|
|
17804
19276
|
}
|
|
17805
19277
|
function maybeSessionExposed(sessionId) {
|
|
17806
19278
|
if (!sessionId) return false;
|
|
17807
19279
|
try {
|
|
17808
|
-
const st =
|
|
19280
|
+
const st = fs19.statSync(file(sessionId));
|
|
17809
19281
|
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
17810
19282
|
} catch (e) {
|
|
17811
19283
|
return e.code !== "ENOENT";
|
|
@@ -17813,27 +19285,7 @@ function maybeSessionExposed(sessionId) {
|
|
|
17813
19285
|
}
|
|
17814
19286
|
|
|
17815
19287
|
// src/exposure/roles.ts
|
|
17816
|
-
import
|
|
17817
|
-
|
|
17818
|
-
// src/daemon/mcp-tools.ts
|
|
17819
|
-
import fs18 from "fs";
|
|
17820
|
-
import path20 from "path";
|
|
17821
|
-
import os14 from "os";
|
|
17822
|
-
function getMcpToolsFile() {
|
|
17823
|
-
return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
|
|
17824
|
-
}
|
|
17825
|
-
function readMcpToolsConfig() {
|
|
17826
|
-
try {
|
|
17827
|
-
const file2 = getMcpToolsFile();
|
|
17828
|
-
if (!fs18.existsSync(file2)) return {};
|
|
17829
|
-
const raw = fs18.readFileSync(file2, "utf-8");
|
|
17830
|
-
return JSON.parse(raw);
|
|
17831
|
-
} catch {
|
|
17832
|
-
return {};
|
|
17833
|
-
}
|
|
17834
|
-
}
|
|
17835
|
-
|
|
17836
|
-
// src/exposure/roles.ts
|
|
19288
|
+
import fs20 from "fs";
|
|
17837
19289
|
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
17838
19290
|
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
17839
19291
|
function foldServerName(name) {
|
|
@@ -17843,7 +19295,7 @@ var inventoryCache = null;
|
|
|
17843
19295
|
var inventoryStamp = null;
|
|
17844
19296
|
function inventoryFileStamp() {
|
|
17845
19297
|
try {
|
|
17846
|
-
const st =
|
|
19298
|
+
const st = fs20.statSync(getMcpToolsFile());
|
|
17847
19299
|
return `${st.mtimeMs}:${st.size}`;
|
|
17848
19300
|
} catch {
|
|
17849
19301
|
return "";
|
|
@@ -17928,15 +19380,15 @@ function sourceWord(toolName) {
|
|
|
17928
19380
|
|
|
17929
19381
|
// src/auth/trusted-recipients.ts
|
|
17930
19382
|
init_dist();
|
|
17931
|
-
import
|
|
17932
|
-
import
|
|
17933
|
-
import
|
|
19383
|
+
import fs21 from "fs";
|
|
19384
|
+
import path22 from "path";
|
|
19385
|
+
import os16 from "os";
|
|
17934
19386
|
function getTrustedRecipientsPath() {
|
|
17935
|
-
return
|
|
19387
|
+
return path22.join(os16.homedir(), ".node9", "trusted-recipients.json");
|
|
17936
19388
|
}
|
|
17937
19389
|
function readTrustedRecipients() {
|
|
17938
19390
|
try {
|
|
17939
|
-
const raw =
|
|
19391
|
+
const raw = fs21.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
17940
19392
|
const parsed = JSON.parse(raw);
|
|
17941
19393
|
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
17942
19394
|
} catch {
|
|
@@ -17945,10 +19397,10 @@ function readTrustedRecipients() {
|
|
|
17945
19397
|
}
|
|
17946
19398
|
function writeTrustedRecipients(entries) {
|
|
17947
19399
|
const filePath = getTrustedRecipientsPath();
|
|
17948
|
-
|
|
19400
|
+
fs21.mkdirSync(path22.dirname(filePath), { recursive: true });
|
|
17949
19401
|
const tmp = filePath + ".node9-tmp";
|
|
17950
|
-
|
|
17951
|
-
|
|
19402
|
+
fs21.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
19403
|
+
fs21.renameSync(tmp, filePath);
|
|
17952
19404
|
}
|
|
17953
19405
|
function addTrustedRecipient(raw, addedBy = "user") {
|
|
17954
19406
|
const value = normalizeRecipient(raw);
|
|
@@ -17963,7 +19415,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
|
|
|
17963
19415
|
var cache3 = null;
|
|
17964
19416
|
function mtimeOf() {
|
|
17965
19417
|
try {
|
|
17966
|
-
return
|
|
19418
|
+
return fs21.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
17967
19419
|
} catch {
|
|
17968
19420
|
return 0;
|
|
17969
19421
|
}
|
|
@@ -17978,40 +19430,40 @@ function isTrustedRecipient(value) {
|
|
|
17978
19430
|
|
|
17979
19431
|
// src/auth/cloud.ts
|
|
17980
19432
|
init_audit();
|
|
17981
|
-
import
|
|
17982
|
-
import
|
|
17983
|
-
import
|
|
19433
|
+
import fs23 from "fs";
|
|
19434
|
+
import os17 from "os";
|
|
19435
|
+
import path23 from "path";
|
|
17984
19436
|
|
|
17985
19437
|
// src/utils/read-capped.ts
|
|
17986
|
-
import
|
|
19438
|
+
import fs22 from "fs";
|
|
17987
19439
|
function readCapped(file2, maxBytes) {
|
|
17988
19440
|
if (maxBytes < 0) return null;
|
|
17989
19441
|
let fd;
|
|
17990
19442
|
try {
|
|
17991
|
-
fd =
|
|
19443
|
+
fd = fs22.openSync(file2, "r");
|
|
17992
19444
|
} catch {
|
|
17993
19445
|
return null;
|
|
17994
19446
|
}
|
|
17995
19447
|
try {
|
|
17996
|
-
const st =
|
|
19448
|
+
const st = fs22.fstatSync(fd);
|
|
17997
19449
|
if (!st.isFile()) return null;
|
|
17998
19450
|
const want = Math.min(st.size, maxBytes + 1);
|
|
17999
19451
|
const buf = Buffer.alloc(want);
|
|
18000
|
-
let
|
|
18001
|
-
while (
|
|
18002
|
-
const n =
|
|
19452
|
+
let read2 = 0;
|
|
19453
|
+
while (read2 < want) {
|
|
19454
|
+
const n = fs22.readSync(fd, buf, read2, want - read2, read2);
|
|
18003
19455
|
if (n <= 0) break;
|
|
18004
|
-
|
|
19456
|
+
read2 += n;
|
|
18005
19457
|
}
|
|
18006
19458
|
return {
|
|
18007
|
-
bytes: buf.subarray(0, Math.min(
|
|
18008
|
-
truncated:
|
|
19459
|
+
bytes: buf.subarray(0, Math.min(read2, maxBytes)),
|
|
19460
|
+
truncated: read2 > maxBytes
|
|
18009
19461
|
};
|
|
18010
19462
|
} catch {
|
|
18011
19463
|
return null;
|
|
18012
19464
|
} finally {
|
|
18013
19465
|
try {
|
|
18014
|
-
|
|
19466
|
+
fs22.closeSync(fd);
|
|
18015
19467
|
} catch {
|
|
18016
19468
|
}
|
|
18017
19469
|
}
|
|
@@ -18056,7 +19508,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18056
19508
|
let ciContext;
|
|
18057
19509
|
if (process.env.CI) {
|
|
18058
19510
|
try {
|
|
18059
|
-
const ciContextPath =
|
|
19511
|
+
const ciContextPath = path23.join(os17.homedir(), ".node9", "ci-context.json");
|
|
18060
19512
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
18061
19513
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
18062
19514
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -18098,9 +19550,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18098
19550
|
context: {
|
|
18099
19551
|
agent: meta?.agent,
|
|
18100
19552
|
mcpServer: meta?.mcpServer,
|
|
18101
|
-
hostname:
|
|
19553
|
+
hostname: os17.hostname(),
|
|
18102
19554
|
cwd: process.cwd(),
|
|
18103
|
-
platform:
|
|
19555
|
+
platform: os17.platform()
|
|
18104
19556
|
},
|
|
18105
19557
|
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
18106
19558
|
// an unknown key, so only fields its schema declares go on the wire.
|
|
@@ -18174,14 +19626,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18174
19626
|
});
|
|
18175
19627
|
clearTimeout(timer);
|
|
18176
19628
|
if (!res.ok) {
|
|
18177
|
-
|
|
19629
|
+
fs23.appendFileSync(
|
|
18178
19630
|
HOOK_DEBUG_LOG,
|
|
18179
19631
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
18180
19632
|
`
|
|
18181
19633
|
);
|
|
18182
19634
|
}
|
|
18183
19635
|
} catch (err) {
|
|
18184
|
-
|
|
19636
|
+
fs23.appendFileSync(
|
|
18185
19637
|
HOOK_DEBUG_LOG,
|
|
18186
19638
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
18187
19639
|
`
|
|
@@ -18192,16 +19644,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18192
19644
|
// src/loop-detector.ts
|
|
18193
19645
|
init_dist();
|
|
18194
19646
|
init_dist();
|
|
18195
|
-
import
|
|
18196
|
-
import
|
|
18197
|
-
import
|
|
19647
|
+
import fs24 from "fs";
|
|
19648
|
+
import path24 from "path";
|
|
19649
|
+
import os18 from "os";
|
|
18198
19650
|
function loopStateFile() {
|
|
18199
|
-
return
|
|
19651
|
+
return path24.join(os18.homedir(), ".node9", "loop-state.json");
|
|
18200
19652
|
}
|
|
18201
19653
|
function readState() {
|
|
18202
19654
|
try {
|
|
18203
|
-
if (!
|
|
18204
|
-
const raw =
|
|
19655
|
+
if (!fs24.existsSync(loopStateFile())) return [];
|
|
19656
|
+
const raw = fs24.readFileSync(loopStateFile(), "utf-8");
|
|
18205
19657
|
const parsed = JSON.parse(raw);
|
|
18206
19658
|
if (!Array.isArray(parsed)) return [];
|
|
18207
19659
|
return parsed;
|
|
@@ -18210,11 +19662,11 @@ function readState() {
|
|
|
18210
19662
|
}
|
|
18211
19663
|
}
|
|
18212
19664
|
function writeState(records) {
|
|
18213
|
-
const dir2 =
|
|
18214
|
-
if (!
|
|
18215
|
-
const tmpPath = `${loopStateFile()}.${
|
|
18216
|
-
|
|
18217
|
-
|
|
19665
|
+
const dir2 = path24.dirname(loopStateFile());
|
|
19666
|
+
if (!fs24.existsSync(dir2)) fs24.mkdirSync(dir2, { recursive: true });
|
|
19667
|
+
const tmpPath = `${loopStateFile()}.${os18.hostname()}.${process.pid}.tmp`;
|
|
19668
|
+
fs24.writeFileSync(tmpPath, JSON.stringify(records));
|
|
19669
|
+
fs24.renameSync(tmpPath, loopStateFile());
|
|
18218
19670
|
}
|
|
18219
19671
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
18220
19672
|
try {
|
|
@@ -18229,20 +19681,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
18229
19681
|
// src/supply-chain/check.ts
|
|
18230
19682
|
init_dist();
|
|
18231
19683
|
import picomatch2 from "picomatch";
|
|
18232
|
-
import
|
|
19684
|
+
import path27 from "path";
|
|
18233
19685
|
|
|
18234
19686
|
// src/supply-chain/osv-index.ts
|
|
18235
19687
|
init_dist();
|
|
18236
|
-
import
|
|
18237
|
-
import
|
|
18238
|
-
import
|
|
19688
|
+
import fs25 from "fs";
|
|
19689
|
+
import os19 from "os";
|
|
19690
|
+
import path25 from "path";
|
|
18239
19691
|
import { createHash as createHash4 } from "crypto";
|
|
18240
19692
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
18241
19693
|
function osvRoot() {
|
|
18242
|
-
return
|
|
19694
|
+
return path25.join(os19.homedir(), ".node9", "osv");
|
|
18243
19695
|
}
|
|
18244
19696
|
function ecoDir(eco) {
|
|
18245
|
-
return
|
|
19697
|
+
return path25.join(osvRoot(), eco);
|
|
18246
19698
|
}
|
|
18247
19699
|
function indexKey(eco, name) {
|
|
18248
19700
|
switch (eco) {
|
|
@@ -18261,11 +19713,11 @@ function shardOf(key) {
|
|
|
18261
19713
|
return createHash4("sha1").update(key).digest("hex").slice(0, 2);
|
|
18262
19714
|
}
|
|
18263
19715
|
function shardPath(eco, shard) {
|
|
18264
|
-
return
|
|
19716
|
+
return path25.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
18265
19717
|
}
|
|
18266
19718
|
function readMeta(eco) {
|
|
18267
19719
|
try {
|
|
18268
|
-
const m = JSON.parse(
|
|
19720
|
+
const m = JSON.parse(fs25.readFileSync(path25.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
18269
19721
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
18270
19722
|
return m;
|
|
18271
19723
|
} catch {
|
|
@@ -18274,7 +19726,7 @@ function readMeta(eco) {
|
|
|
18274
19726
|
}
|
|
18275
19727
|
function readShard(eco, shard) {
|
|
18276
19728
|
try {
|
|
18277
|
-
const s = JSON.parse(
|
|
19729
|
+
const s = JSON.parse(fs25.readFileSync(shardPath(eco, shard), "utf8"));
|
|
18278
19730
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
18279
19731
|
} catch {
|
|
18280
19732
|
return {};
|
|
@@ -18401,13 +19853,101 @@ async function queryOsvMalicious(queries) {
|
|
|
18401
19853
|
|
|
18402
19854
|
// src/supply-chain/registry.ts
|
|
18403
19855
|
import { maxSatisfying, satisfies, validRange } from "semver";
|
|
19856
|
+
|
|
19857
|
+
// src/supply-chain/provenance.ts
|
|
19858
|
+
import { prerelease, rcompare, valid } from "semver";
|
|
18404
19859
|
var TIMEOUT_MS2 = 1500;
|
|
18405
|
-
var MAX_BYTES =
|
|
19860
|
+
var MAX_BYTES = 256 * 1024;
|
|
19861
|
+
function normalizeRepo(raw, host2 = "github.com") {
|
|
19862
|
+
let s = raw.trim().toLowerCase();
|
|
19863
|
+
s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
|
|
19864
|
+
s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
|
|
19865
|
+
if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
|
|
19866
|
+
return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
|
|
19867
|
+
}
|
|
19868
|
+
function npmRepoOf(doc) {
|
|
19869
|
+
const list = doc?.attestations;
|
|
19870
|
+
if (!Array.isArray(list)) return null;
|
|
19871
|
+
for (const a of list) {
|
|
19872
|
+
const att = a;
|
|
19873
|
+
if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
|
|
19874
|
+
const payload = att.bundle?.dsseEnvelope?.payload;
|
|
19875
|
+
if (typeof payload !== "string") continue;
|
|
19876
|
+
try {
|
|
19877
|
+
const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
|
19878
|
+
const p = statement.predicate;
|
|
19879
|
+
const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
|
|
19880
|
+
if (typeof repo === "string") return normalizeRepo(repo);
|
|
19881
|
+
} catch {
|
|
19882
|
+
}
|
|
19883
|
+
}
|
|
19884
|
+
return null;
|
|
19885
|
+
}
|
|
19886
|
+
function pypiRepoOf(doc) {
|
|
19887
|
+
const bundles = doc?.attestation_bundles;
|
|
19888
|
+
if (!Array.isArray(bundles)) return null;
|
|
19889
|
+
for (const b of bundles) {
|
|
19890
|
+
const pub = b.publisher;
|
|
19891
|
+
if (typeof pub?.repository === "string") {
|
|
19892
|
+
const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
|
|
19893
|
+
return normalizeRepo(pub.repository, host2);
|
|
19894
|
+
}
|
|
19895
|
+
if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
|
|
19896
|
+
return `google:${pub.email.trim().toLowerCase()}`;
|
|
19897
|
+
}
|
|
19898
|
+
}
|
|
19899
|
+
return null;
|
|
19900
|
+
}
|
|
19901
|
+
async function npmSourceRepo(name, version) {
|
|
19902
|
+
const base2 = endpoints().npmRegistry;
|
|
19903
|
+
if (!base2) return "unknown";
|
|
19904
|
+
const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
|
|
19905
|
+
return read(url, npmRepoOf);
|
|
19906
|
+
}
|
|
19907
|
+
async function read(url, repoOf) {
|
|
19908
|
+
try {
|
|
19909
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
|
|
19910
|
+
if (doc === null) return "none";
|
|
19911
|
+
return repoOf(doc) ?? "unknown";
|
|
19912
|
+
} catch {
|
|
19913
|
+
return "unknown";
|
|
19914
|
+
}
|
|
19915
|
+
}
|
|
19916
|
+
async function pypiSourceRepo(name, version, filename) {
|
|
19917
|
+
const base2 = endpoints().pypi;
|
|
19918
|
+
if (!base2) return "unknown";
|
|
19919
|
+
const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
|
|
19920
|
+
return read(url, pypiRepoOf);
|
|
19921
|
+
}
|
|
19922
|
+
var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
|
|
19923
|
+
function comparePypi(a, b) {
|
|
19924
|
+
const x = a.split(".").map(Number);
|
|
19925
|
+
const y = b.split(".").map(Number);
|
|
19926
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
19927
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
19928
|
+
if (d !== 0) return d;
|
|
19929
|
+
}
|
|
19930
|
+
return 0;
|
|
19931
|
+
}
|
|
19932
|
+
function previousRelease(eco, versions, version) {
|
|
19933
|
+
if (eco === "npm") {
|
|
19934
|
+
if (!valid(version)) return void 0;
|
|
19935
|
+
const below2 = versions.filter((v) => valid(v) && !prerelease(v) && rcompare(v, version) > 0);
|
|
19936
|
+
return below2.sort(rcompare)[0] ?? null;
|
|
19937
|
+
}
|
|
19938
|
+
if (!PYPI_FINAL.test(version)) return void 0;
|
|
19939
|
+
const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
|
|
19940
|
+
return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
|
|
19941
|
+
}
|
|
19942
|
+
|
|
19943
|
+
// src/supply-chain/registry.ts
|
|
19944
|
+
var TIMEOUT_MS3 = 1500;
|
|
19945
|
+
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
18406
19946
|
var VERSION_DOC_MAX_BYTES = 256 * 1024;
|
|
18407
19947
|
var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
18408
19948
|
async function npmVersionDocument(url, selector) {
|
|
18409
19949
|
const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
|
|
18410
|
-
timeoutMs:
|
|
19950
|
+
timeoutMs: TIMEOUT_MS3,
|
|
18411
19951
|
maxBytes: VERSION_DOC_MAX_BYTES
|
|
18412
19952
|
});
|
|
18413
19953
|
if (!doc?.version) return null;
|
|
@@ -18431,8 +19971,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18431
19971
|
let doc;
|
|
18432
19972
|
try {
|
|
18433
19973
|
doc = await fetchJson(url, {
|
|
18434
|
-
timeoutMs:
|
|
18435
|
-
maxBytes:
|
|
19974
|
+
timeoutMs: TIMEOUT_MS3,
|
|
19975
|
+
maxBytes: MAX_BYTES2,
|
|
18436
19976
|
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
18437
19977
|
});
|
|
18438
19978
|
} catch (err) {
|
|
@@ -18460,13 +20000,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18460
20000
|
version: resolved,
|
|
18461
20001
|
hasInstallScript: v?.hasInstallScript === true
|
|
18462
20002
|
};
|
|
20003
|
+
const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
|
|
20004
|
+
if (v && previous !== void 0) {
|
|
20005
|
+
const pv = previous ? doc.versions?.[previous] : void 0;
|
|
20006
|
+
info.history = {
|
|
20007
|
+
previous,
|
|
20008
|
+
previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
|
|
20009
|
+
provenance: !!v.dist?.attestations?.provenance,
|
|
20010
|
+
previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
|
|
20011
|
+
};
|
|
20012
|
+
}
|
|
18463
20013
|
if (!Number.isFinite(modifiedMs)) return info;
|
|
18464
20014
|
if (Date.now() - modifiedMs >= freshWindowMs) {
|
|
18465
20015
|
info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
|
|
18466
20016
|
return info;
|
|
18467
20017
|
}
|
|
18468
20018
|
try {
|
|
18469
|
-
const full = await fetchJson(url, { timeoutMs:
|
|
20019
|
+
const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18470
20020
|
const t = full?.time?.[resolved];
|
|
18471
20021
|
const tMs = t ? Date.parse(t) : NaN;
|
|
18472
20022
|
if (Number.isFinite(tMs)) {
|
|
@@ -18484,15 +20034,37 @@ async function pypiInfo(name, version) {
|
|
|
18484
20034
|
const base2 = endpoints().pypi;
|
|
18485
20035
|
if (!base2) return null;
|
|
18486
20036
|
const url = version ? `${base2}/pypi/${encodeURIComponent(name)}/${encodeURIComponent(version)}/json` : `${base2}/pypi/${encodeURIComponent(name)}/json`;
|
|
18487
|
-
const doc = await fetchJson(url, { timeoutMs:
|
|
20037
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18488
20038
|
if (!doc?.info?.version) return null;
|
|
18489
20039
|
const times = (doc.urls ?? []).map((u) => u.upload_time_iso_8601 ? Date.parse(u.upload_time_iso_8601) : NaN).filter((t) => Number.isFinite(t));
|
|
18490
|
-
|
|
18491
|
-
|
|
20040
|
+
const resolved = version ?? doc.info.version;
|
|
20041
|
+
const info = {
|
|
20042
|
+
version: resolved,
|
|
18492
20043
|
// The earliest file of the release is when it became installable.
|
|
18493
|
-
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
|
|
20044
|
+
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
|
|
20045
|
+
file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
|
|
20046
|
+
};
|
|
20047
|
+
if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
|
|
20048
|
+
return info;
|
|
20049
|
+
}
|
|
20050
|
+
function pypiHistoryOf(releases, version) {
|
|
20051
|
+
const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
|
|
20052
|
+
const previous = previousRelease("PyPI", versions, version);
|
|
20053
|
+
if (previous === void 0) return null;
|
|
20054
|
+
return {
|
|
20055
|
+
previous,
|
|
20056
|
+
previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
|
|
18494
20057
|
};
|
|
18495
20058
|
}
|
|
20059
|
+
async function pypiHistory(name, version) {
|
|
20060
|
+
const base2 = endpoints().pypi;
|
|
20061
|
+
if (!base2) return null;
|
|
20062
|
+
const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
|
|
20063
|
+
timeoutMs: TIMEOUT_MS3,
|
|
20064
|
+
maxBytes: MAX_BYTES2
|
|
20065
|
+
});
|
|
20066
|
+
return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
|
|
20067
|
+
}
|
|
18496
20068
|
var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
|
|
18497
20069
|
async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
18498
20070
|
if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
|
|
@@ -18501,19 +20073,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
18501
20073
|
}
|
|
18502
20074
|
|
|
18503
20075
|
// src/supply-chain/local-resolve.ts
|
|
18504
|
-
import
|
|
18505
|
-
import
|
|
20076
|
+
import fs26 from "fs";
|
|
20077
|
+
import path26 from "path";
|
|
18506
20078
|
var MAX_LEVELS = 32;
|
|
18507
20079
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
18508
20080
|
function resolveInstalled(name, cwd) {
|
|
18509
|
-
if (!cwd || !
|
|
20081
|
+
if (!cwd || !path26.isAbsolute(cwd)) return null;
|
|
18510
20082
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
18511
|
-
let dir2 =
|
|
20083
|
+
let dir2 = path26.resolve(cwd);
|
|
18512
20084
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
18513
|
-
const pkgDir =
|
|
18514
|
-
const manifest =
|
|
20085
|
+
const pkgDir = path26.join(dir2, "node_modules", ...name.split("/"));
|
|
20086
|
+
const manifest = path26.join(pkgDir, "package.json");
|
|
18515
20087
|
try {
|
|
18516
|
-
const raw =
|
|
20088
|
+
const raw = fs26.readFileSync(manifest, "utf8");
|
|
18517
20089
|
const parsed = JSON.parse(raw);
|
|
18518
20090
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
18519
20091
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -18521,7 +20093,7 @@ function resolveInstalled(name, cwd) {
|
|
|
18521
20093
|
return null;
|
|
18522
20094
|
} catch {
|
|
18523
20095
|
}
|
|
18524
|
-
const parent =
|
|
20096
|
+
const parent = path26.dirname(dir2);
|
|
18525
20097
|
if (parent === dir2) break;
|
|
18526
20098
|
dir2 = parent;
|
|
18527
20099
|
}
|
|
@@ -18643,7 +20215,7 @@ function checkInstalled(p, version, misses) {
|
|
|
18643
20215
|
}
|
|
18644
20216
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
18645
20217
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
18646
|
-
const localCwd = cwd &&
|
|
20218
|
+
const localCwd = cwd && path27.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path27.resolve(dir2, step), cwd) : void 0;
|
|
18647
20219
|
const installed = resolveInstalled(p.name, localCwd);
|
|
18648
20220
|
if (installed && (!p.version || p.version === installed.version))
|
|
18649
20221
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -18686,8 +20258,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
|
18686
20258
|
detail: `${label(p, version)} runs an install script`
|
|
18687
20259
|
});
|
|
18688
20260
|
}
|
|
20261
|
+
if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
|
|
20262
|
+
return byHistory(p, version, info, findings, misses);
|
|
20263
|
+
}
|
|
18689
20264
|
return findings;
|
|
18690
20265
|
}
|
|
20266
|
+
var SOFT2 = /* @__PURE__ */ new Set(["new", "install-script"]);
|
|
20267
|
+
async function byHistory(p, version, info, findings, misses) {
|
|
20268
|
+
let out = findings;
|
|
20269
|
+
let h = info.history;
|
|
20270
|
+
if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT2.has(f.kind))) {
|
|
20271
|
+
try {
|
|
20272
|
+
h = await pypiHistory(p.name, version) ?? void 0;
|
|
20273
|
+
} catch (err) {
|
|
20274
|
+
misses.push(`${label(p, version)}: release history ${err.message}`);
|
|
20275
|
+
}
|
|
20276
|
+
}
|
|
20277
|
+
if (!h || !h.previous) return out;
|
|
20278
|
+
const previous = h.previous;
|
|
20279
|
+
if (h.previousHasInstallScript === true) {
|
|
20280
|
+
out = out.filter((f) => f.kind !== "install-script");
|
|
20281
|
+
}
|
|
20282
|
+
const drop = () => ({
|
|
20283
|
+
pkg: p,
|
|
20284
|
+
version,
|
|
20285
|
+
kind: "provenance-drop",
|
|
20286
|
+
detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
|
|
20287
|
+
});
|
|
20288
|
+
if (p.ecosystem === "npm") {
|
|
20289
|
+
if (h.previousProvenance && h.provenance === false) return [...out, drop()];
|
|
20290
|
+
if (!out.some((f) => SOFT2.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
|
|
20291
|
+
const [now, before] = await Promise.all([
|
|
20292
|
+
npmSourceRepo(p.name, version),
|
|
20293
|
+
npmSourceRepo(p.name, previous)
|
|
20294
|
+
]);
|
|
20295
|
+
return vouched(p, version, now, before, out, misses);
|
|
20296
|
+
}
|
|
20297
|
+
if (p.ecosystem === "PyPI") {
|
|
20298
|
+
if (!out.some((f) => SOFT2.has(f.kind)) || !info.file || !h.previousFile) return out;
|
|
20299
|
+
const [now, before] = await Promise.all([
|
|
20300
|
+
pypiSourceRepo(p.name, version, info.file),
|
|
20301
|
+
pypiSourceRepo(p.name, previous, h.previousFile)
|
|
20302
|
+
]);
|
|
20303
|
+
if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
|
|
20304
|
+
return vouched(p, version, now, before, out, misses);
|
|
20305
|
+
}
|
|
20306
|
+
return out;
|
|
20307
|
+
}
|
|
20308
|
+
function vouched(p, version, now, before, findings, misses) {
|
|
20309
|
+
if (now === "unknown" || before === "unknown") {
|
|
20310
|
+
misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
|
|
20311
|
+
return findings;
|
|
20312
|
+
}
|
|
20313
|
+
if (now === "none" || before === "none" || now !== before) return findings;
|
|
20314
|
+
return findings.filter((f) => !SOFT2.has(f.kind));
|
|
20315
|
+
}
|
|
18691
20316
|
async function runPackageCheck(toolName, args, cfg, cwd) {
|
|
18692
20317
|
if (!cfg.enabled) return ALLOW;
|
|
18693
20318
|
try {
|
|
@@ -19006,7 +20631,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19006
20631
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
19007
20632
|
}
|
|
19008
20633
|
const isManual = meta?.agent === "Terminal";
|
|
19009
|
-
const isObserveMode = config.settings.mode
|
|
20634
|
+
const isObserveMode = isRecordOnly(config.settings.mode);
|
|
19010
20635
|
let explainableLabel = "Local Config";
|
|
19011
20636
|
let dlpReviewFlagged = false;
|
|
19012
20637
|
let packageReview = null;
|
|
@@ -19064,13 +20689,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19064
20689
|
explainableLabel = "Shell inspection incomplete";
|
|
19065
20690
|
policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
|
|
19066
20691
|
}
|
|
19067
|
-
const fileReview = !isObserveMode &&
|
|
20692
|
+
const fileReview = !isObserveMode && fileFinding?.decision === "review";
|
|
19068
20693
|
const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
|
|
19069
20694
|
const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
19070
20695
|
fileAccess,
|
|
19071
20696
|
config,
|
|
19072
20697
|
skipIgnoredFastPath,
|
|
19073
|
-
argumentScan: argScan
|
|
20698
|
+
argumentScan: argScan,
|
|
20699
|
+
// A server a person linked to an app is judged as that app (fw#390).
|
|
20700
|
+
mcpServer: resolveLinkedServer(meta?.mcpServer, {
|
|
20701
|
+
serverKey: meta?.serverKey,
|
|
20702
|
+
managedLinks: config.policy.appLinks,
|
|
20703
|
+
appliedShields: config.policy.appliedShields,
|
|
20704
|
+
workspaceGoverned: config.policySource === "workspace"
|
|
20705
|
+
})
|
|
19074
20706
|
});
|
|
19075
20707
|
if (!sessionScript || own.decision === "block") return own;
|
|
19076
20708
|
const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
|
|
@@ -19084,7 +20716,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19084
20716
|
if (fileFinding) {
|
|
19085
20717
|
meta = { ...meta, ruleName: fileFinding.ruleName };
|
|
19086
20718
|
hashAuditArgs = true;
|
|
19087
|
-
const observing = isObserveMode
|
|
20719
|
+
const observing = isObserveMode;
|
|
19088
20720
|
const blocked = !observing && (fileFinding.decision === "block" || fileReview && config.settings.panicMode);
|
|
19089
20721
|
appendLocalAudit(
|
|
19090
20722
|
toolName,
|
|
@@ -19286,7 +20918,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19286
20918
|
const last = rec.sources[rec.sources.length - 1];
|
|
19287
20919
|
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
19288
20920
|
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
19289
|
-
const
|
|
20921
|
+
const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
19290
20922
|
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
19291
20923
|
const noun = (d) => {
|
|
19292
20924
|
const k = destinationKind(d);
|
|
@@ -19295,14 +20927,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19295
20927
|
const parts = held.map(
|
|
19296
20928
|
(h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
|
|
19297
20929
|
);
|
|
19298
|
-
const home =
|
|
20930
|
+
const home = os20.homedir();
|
|
19299
20931
|
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
19300
20932
|
for (const w of writeHolds) {
|
|
19301
20933
|
parts.push(
|
|
19302
20934
|
w.why === "outside-project" ? `writes outside the project, to ${short(w.path)}` : w.why === "agent-instruction" ? `writes to ${short(w.path)}, a file that runs code or instructs the agent` : `writes content carrying ${destinationValue(w.destination ?? "")}, ${noun(w.destination ?? "host:x")} that appeared in that ${readWord}`
|
|
19303
20935
|
);
|
|
19304
20936
|
}
|
|
19305
|
-
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${
|
|
20937
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
|
|
19306
20938
|
exposureFromEid = last?.eid;
|
|
19307
20939
|
if (exposureFromEid) exposureMeta = { exposureFromEid };
|
|
19308
20940
|
exposureKeys = [
|
|
@@ -19566,15 +21198,6 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19566
21198
|
}
|
|
19567
21199
|
return { approved: true, checkedBy: "audit" };
|
|
19568
21200
|
}
|
|
19569
|
-
if (config.settings.mode === "audit") {
|
|
19570
|
-
if (judge) {
|
|
19571
|
-
const policyResult = await evaluateCurrentPolicy();
|
|
19572
|
-
if (policyResult.decision === "review") {
|
|
19573
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", ownRowMeta(true), hashAuditArgs);
|
|
19574
|
-
}
|
|
19575
|
-
}
|
|
19576
|
-
return { approved: true, checkedBy: "audit" };
|
|
19577
|
-
}
|
|
19578
21201
|
let appPermReview = null;
|
|
19579
21202
|
let appPermReviewTool = null;
|
|
19580
21203
|
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;
|