@node9/proxy 2.37.1 → 2.38.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 +6835 -3672
- package/dist/cli.mjs +6725 -3561
- package/dist/dashboard.mjs +1198 -360
- package/dist/index.js +2150 -664
- package/dist/index.mjs +2146 -660
- package/dist/scan-ink.mjs +1308 -269
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -71,6 +71,93 @@ var init_hasher = __esm({
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
// packages/policy-engine/dist/index.mjs
|
|
74
|
+
function validateRegex(pattern) {
|
|
75
|
+
if (!pattern) return "Pattern is required";
|
|
76
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
77
|
+
try {
|
|
78
|
+
new RegExp(pattern);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
81
|
+
}
|
|
82
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
83
|
+
if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
87
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
88
|
+
const key = `${pattern}\0${flags}`;
|
|
89
|
+
if (regexCache.has(key)) {
|
|
90
|
+
const cached = regexCache.get(key);
|
|
91
|
+
regexCache.delete(key);
|
|
92
|
+
regexCache.set(key, cached);
|
|
93
|
+
return cached;
|
|
94
|
+
}
|
|
95
|
+
if (validateRegex(pattern) !== null) return null;
|
|
96
|
+
try {
|
|
97
|
+
const re = new RegExp(pattern, flags);
|
|
98
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
99
|
+
const oldest = regexCache.keys().next().value;
|
|
100
|
+
if (oldest) regexCache.delete(oldest);
|
|
101
|
+
}
|
|
102
|
+
regexCache.set(key, re);
|
|
103
|
+
return re;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function isToolRule(rule) {
|
|
109
|
+
return rule.tool.startsWith("{,mcp__*__}");
|
|
110
|
+
}
|
|
111
|
+
function actionPart(ruleName) {
|
|
112
|
+
const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
|
|
113
|
+
return m ? m[1] : ruleName ?? "rule";
|
|
114
|
+
}
|
|
115
|
+
function ownReason(reason, owner) {
|
|
116
|
+
if (!reason) return reason;
|
|
117
|
+
return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
|
|
118
|
+
}
|
|
119
|
+
function buildAppShield(spec) {
|
|
120
|
+
const owner = spec.label;
|
|
121
|
+
const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
|
|
122
|
+
...rule,
|
|
123
|
+
name: `shield:${spec.name}:${actionPart(rule.name)}`,
|
|
124
|
+
server: spec.server,
|
|
125
|
+
reason: ownReason(rule.reason, owner)
|
|
126
|
+
}));
|
|
127
|
+
const ownTools = (spec.mcpTools ?? []).map((rule) => ({
|
|
128
|
+
...rule,
|
|
129
|
+
server: spec.server
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
name: spec.name,
|
|
133
|
+
description: spec.description,
|
|
134
|
+
aliases: [],
|
|
135
|
+
smartRules: [...toolRules, ...ownTools, ...spec.doors],
|
|
136
|
+
dangerousWords: []
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function buildGeneralRules() {
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const name of Object.keys(ACTION_TEMPLATES)) {
|
|
142
|
+
const { slug: slug2, label: label2 } = ACTION_CHECKS[name];
|
|
143
|
+
for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
|
|
144
|
+
const part = actionPart(rule.name).replace(/^review-/, "");
|
|
145
|
+
out.push({
|
|
146
|
+
...rule,
|
|
147
|
+
name: `review-action-${slug2}-${part}`,
|
|
148
|
+
notServer: APP_SERVER_GLOB,
|
|
149
|
+
reason: ownReason(rule.reason, label2)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function generalActionRuleNames(template) {
|
|
156
|
+
const { slug: slug2 } = ACTION_CHECKS[template];
|
|
157
|
+
return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
|
|
158
|
+
(r) => r.name
|
|
159
|
+
);
|
|
160
|
+
}
|
|
74
161
|
function isShieldVerdict(v) {
|
|
75
162
|
return v === "allow" || v === "review" || v === "block";
|
|
76
163
|
}
|
|
@@ -108,16 +195,25 @@ function validateOverrides(raw) {
|
|
|
108
195
|
return { overrides: result, warnings };
|
|
109
196
|
}
|
|
110
197
|
function assertBuiltinShieldRegexesAreSafe() {
|
|
111
|
-
|
|
198
|
+
const sets = [
|
|
199
|
+
...Object.values(BUILTIN_SHIELDS).map((sh) => ({
|
|
200
|
+
name: sh.name,
|
|
201
|
+
smartRules: sh.smartRules
|
|
202
|
+
})),
|
|
203
|
+
...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
|
|
204
|
+
{ name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
|
|
205
|
+
];
|
|
206
|
+
for (const shield of sets) {
|
|
112
207
|
for (const rule of shield.smartRules) {
|
|
113
208
|
const conditions = rule.conditions ?? [];
|
|
114
209
|
for (const cond of conditions) {
|
|
115
210
|
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
116
211
|
const pattern = cond.value;
|
|
117
212
|
if (!pattern) continue;
|
|
118
|
-
|
|
213
|
+
const problem = validateRegex(pattern);
|
|
214
|
+
if (problem) {
|
|
119
215
|
throw new Error(
|
|
120
|
-
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has
|
|
216
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
|
|
121
217
|
);
|
|
122
218
|
}
|
|
123
219
|
}
|
|
@@ -403,7 +499,7 @@ function isLockedCheck(def) {
|
|
|
403
499
|
function ssrfCheckId(tier) {
|
|
404
500
|
return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
|
|
405
501
|
}
|
|
406
|
-
function
|
|
502
|
+
function ruleNameInLabel(s) {
|
|
407
503
|
let rest = s;
|
|
408
504
|
const override = rest.lastIndexOf(OVERRIDE_PREFIX);
|
|
409
505
|
if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
|
|
@@ -419,7 +515,7 @@ function checkIdForRule(nameOrLabel) {
|
|
|
419
515
|
const s = nameOrLabel.trim();
|
|
420
516
|
const exact = LEGACY_TO_ID.get(s);
|
|
421
517
|
if (exact) return exact;
|
|
422
|
-
const unwrapped =
|
|
518
|
+
const unwrapped = ruleNameInLabel(s);
|
|
423
519
|
if (unwrapped !== void 0) return checkIdForRule(unwrapped);
|
|
424
520
|
if (s.startsWith("shield:project-jail:")) {
|
|
425
521
|
return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
|
|
@@ -507,9 +603,9 @@ function resolveCheck(settings, id) {
|
|
|
507
603
|
const on = settings.appliedShields?.includes(def.pack) ?? false;
|
|
508
604
|
return { id, value: on ? def.defaultValue : "off", source: "default" };
|
|
509
605
|
}
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
512
|
-
const value = CHECK_VERDICT_RANK[
|
|
606
|
+
const read2 = LEGACY_READERS[id]?.(settings);
|
|
607
|
+
if (read2 === void 0) return { id, value: def.defaultValue, source: "default" };
|
|
608
|
+
const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
|
|
513
609
|
return { id, value, source: value === def.defaultValue ? "default" : "configured" };
|
|
514
610
|
}
|
|
515
611
|
function checkValue(settings, id) {
|
|
@@ -577,6 +673,102 @@ function scanViews(text) {
|
|
|
577
673
|
}
|
|
578
674
|
return views;
|
|
579
675
|
}
|
|
676
|
+
function isAgentInstructionPath(absPath, root) {
|
|
677
|
+
const r = root.replace(/\/$/, "");
|
|
678
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
679
|
+
const rel = absPath.slice(r.length + 1);
|
|
680
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
681
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
682
|
+
}
|
|
683
|
+
function isInside(root, p) {
|
|
684
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
685
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
686
|
+
}
|
|
687
|
+
function isPasswordHashPath(p) {
|
|
688
|
+
return PASSWORD_HASH_FILES.includes(p);
|
|
689
|
+
}
|
|
690
|
+
function isLegacySystemPath(p) {
|
|
691
|
+
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
692
|
+
}
|
|
693
|
+
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
694
|
+
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
695
|
+
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
696
|
+
const rooted = p.startsWith("/");
|
|
697
|
+
const parts = [];
|
|
698
|
+
for (const part of p.split("/")) {
|
|
699
|
+
if (!part || part === ".") continue;
|
|
700
|
+
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
701
|
+
else if (part !== ".." || !rooted) parts.push(part);
|
|
702
|
+
}
|
|
703
|
+
return (rooted ? "/" : "") + parts.join("/");
|
|
704
|
+
}
|
|
705
|
+
function inlineLanguageOf(interp) {
|
|
706
|
+
const i = interp.toLowerCase();
|
|
707
|
+
if (/^python[\d.]*$/.test(i)) return "python";
|
|
708
|
+
if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
|
|
709
|
+
if (i === "perl") return "perl";
|
|
710
|
+
if (i === "ruby" || i === "irb") return "ruby";
|
|
711
|
+
if (i === "php") return "php";
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function resolvePythonAliases(code) {
|
|
715
|
+
const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
|
|
716
|
+
let out = code;
|
|
717
|
+
for (const [, mod, alias] of aliases) {
|
|
718
|
+
if (alias === mod) continue;
|
|
719
|
+
out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
}
|
|
723
|
+
function protectedLiteral(code, isProtected) {
|
|
724
|
+
let n = 0;
|
|
725
|
+
for (const m of code.matchAll(STRING_LITERAL)) {
|
|
726
|
+
if (++n > 2048) break;
|
|
727
|
+
const value = m[2].trim();
|
|
728
|
+
if (value && isProtected(value)) return value;
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
function amplifies(language, code) {
|
|
733
|
+
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*\)/;
|
|
734
|
+
const m = loop.exec(code);
|
|
735
|
+
if (!m) return null;
|
|
736
|
+
const body = code.slice(m.index);
|
|
737
|
+
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(
|
|
738
|
+
body
|
|
739
|
+
) ? m[0] : null;
|
|
740
|
+
}
|
|
741
|
+
function judgeInlineCode(language, code, isProtected) {
|
|
742
|
+
if (code.length > MAX_INLINE_CODE) {
|
|
743
|
+
return { outcome: "unreadable", what: "is too long for node9 to read" };
|
|
744
|
+
}
|
|
745
|
+
const text = language === "python" ? resolvePythonAliases(code) : code;
|
|
746
|
+
for (const [category, re] of TABLES[language]) {
|
|
747
|
+
const m = re.exec(text);
|
|
748
|
+
if (!m) continue;
|
|
749
|
+
const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
|
|
750
|
+
return {
|
|
751
|
+
outcome: category === "builds-code" ? "unreadable" : "dangerous",
|
|
752
|
+
what: WHAT[category],
|
|
753
|
+
evidence,
|
|
754
|
+
category
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const path28 = protectedLiteral(text, isProtected);
|
|
758
|
+
if (path28) {
|
|
759
|
+
return {
|
|
760
|
+
outcome: "dangerous",
|
|
761
|
+
what: WHAT["reads-protected"],
|
|
762
|
+
evidence: path28.slice(0, 60),
|
|
763
|
+
category: "reads-protected"
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const loop = amplifies(language, text);
|
|
767
|
+
if (loop) {
|
|
768
|
+
return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
580
772
|
function isCatHeredocOrLit(part) {
|
|
581
773
|
if (!part) return false;
|
|
582
774
|
const t = syntax.NodeType(part);
|
|
@@ -645,6 +837,55 @@ function cachedNormalize(command, compute) {
|
|
|
645
837
|
function normalizeCommandForPolicy(command) {
|
|
646
838
|
return commandReadingsImpl(command).posix;
|
|
647
839
|
}
|
|
840
|
+
function fileAccessView(command) {
|
|
841
|
+
const hit = accessViewCache.get(command);
|
|
842
|
+
if (hit !== void 0) return hit;
|
|
843
|
+
const view = fileAccessViewImpl(command);
|
|
844
|
+
if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
|
|
845
|
+
const oldest = accessViewCache.keys().next().value;
|
|
846
|
+
if (oldest !== void 0) accessViewCache.delete(oldest);
|
|
847
|
+
}
|
|
848
|
+
accessViewCache.set(command, view);
|
|
849
|
+
return view;
|
|
850
|
+
}
|
|
851
|
+
function fileAccessViewImpl(command) {
|
|
852
|
+
const text = command.replace(INTERPRETER_LINE, "");
|
|
853
|
+
const f = parseShared(text);
|
|
854
|
+
if (f === PARSE_FAIL) return text;
|
|
855
|
+
const toCharIndex = byteOffsetToCharIndex(text);
|
|
856
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
857
|
+
const edits = [];
|
|
858
|
+
try {
|
|
859
|
+
syntax.Walk(f, (node) => {
|
|
860
|
+
if (!node) return false;
|
|
861
|
+
const n = node;
|
|
862
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
863
|
+
const args = n.Args || [];
|
|
864
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
865
|
+
const head = unwrapCommandHead(words);
|
|
866
|
+
for (const i of /* @__PURE__ */ new Set([0, head])) {
|
|
867
|
+
const word = words[i];
|
|
868
|
+
if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (!PROGRAM_DIR.test(word)) continue;
|
|
872
|
+
const name = word.split(/[\\/]/).pop() ?? "";
|
|
873
|
+
if (!name) continue;
|
|
874
|
+
const s = at(args[i].Pos().Offset());
|
|
875
|
+
const e = at(args[i].End().Offset());
|
|
876
|
+
if (s < 0 || e < 0) continue;
|
|
877
|
+
edits.push([s, e, name]);
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
});
|
|
881
|
+
} catch {
|
|
882
|
+
return text;
|
|
883
|
+
}
|
|
884
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
885
|
+
let out = text;
|
|
886
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
648
889
|
function commandReadings(command) {
|
|
649
890
|
const r = commandReadingsImpl(command);
|
|
650
891
|
const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
|
|
@@ -1100,53 +1341,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
|
|
|
1100
1341
|
}
|
|
1101
1342
|
return i;
|
|
1102
1343
|
}
|
|
1103
|
-
function
|
|
1344
|
+
function heredocText(word) {
|
|
1345
|
+
const parts = word?.Parts ?? [];
|
|
1346
|
+
let out = "";
|
|
1347
|
+
for (const p of parts) {
|
|
1348
|
+
if (syntax.NodeType(p) !== "Lit") return null;
|
|
1349
|
+
out += p.Value ?? "";
|
|
1350
|
+
}
|
|
1351
|
+
return out;
|
|
1352
|
+
}
|
|
1353
|
+
function stdinCodeOf(redirs) {
|
|
1354
|
+
const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
|
|
1355
|
+
const herestring = deriveRedirOp("cat <<< x");
|
|
1356
|
+
for (const r of redirs) {
|
|
1357
|
+
if (!r || !redirStdinOps().has(r.Op)) continue;
|
|
1358
|
+
if (heredoc.has(r.Op)) {
|
|
1359
|
+
const text = heredocText(r.Hdoc);
|
|
1360
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1361
|
+
}
|
|
1362
|
+
if (r.Op === herestring) {
|
|
1363
|
+
const text = resolveWordLiteral(r.Word);
|
|
1364
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1365
|
+
}
|
|
1366
|
+
return { code: null, source: "file" };
|
|
1367
|
+
}
|
|
1368
|
+
return { code: null, source: "file" };
|
|
1369
|
+
}
|
|
1370
|
+
function attachedCode(interp, flag) {
|
|
1371
|
+
const eq = flag.indexOf("=");
|
|
1372
|
+
if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
|
|
1373
|
+
if (/^(pwsh|powershell)/.test(interp)) return null;
|
|
1374
|
+
const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1375
|
+
const body = flag.slice(1);
|
|
1376
|
+
const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
|
|
1377
|
+
if (at < 0) return null;
|
|
1378
|
+
let rest = body.slice(at + 1);
|
|
1379
|
+
if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
|
|
1380
|
+
return rest === "" ? null : rest;
|
|
1381
|
+
}
|
|
1382
|
+
function inlineExecCall(stmt, pipeFed) {
|
|
1104
1383
|
const cmd = stmt?.Cmd;
|
|
1105
|
-
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return
|
|
1384
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
|
|
1106
1385
|
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1107
|
-
if (words.length === 0) return
|
|
1386
|
+
if (words.length === 0) return null;
|
|
1108
1387
|
const headIdx = unwrapCommandHead(words);
|
|
1109
1388
|
const rawHead = words[headIdx];
|
|
1110
|
-
if (rawHead == null) return
|
|
1389
|
+
if (rawHead == null) return null;
|
|
1111
1390
|
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1112
|
-
if (!INLINE_INTERPRETER.test(interp)) return
|
|
1391
|
+
if (!INLINE_INTERPRETER.test(interp)) return null;
|
|
1113
1392
|
let args = words.slice(headIdx + 1);
|
|
1114
|
-
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval")
|
|
1393
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
|
|
1394
|
+
const code = args[1] ?? null;
|
|
1395
|
+
return { interp, code: [code], source: code === null ? "variable" : void 0 };
|
|
1396
|
+
}
|
|
1115
1397
|
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1116
1398
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1117
1399
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1118
1400
|
}
|
|
1401
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1402
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1403
|
+
const fromStdin = () => {
|
|
1404
|
+
if (stdinFed) {
|
|
1405
|
+
const { code, source } = stdinCodeOf(redirs);
|
|
1406
|
+
return { interp, code: [code], source };
|
|
1407
|
+
}
|
|
1408
|
+
return { interp, code: [null], source: "pipe" };
|
|
1409
|
+
};
|
|
1119
1410
|
let positionals2 = 0;
|
|
1120
1411
|
let selectedProgram = false;
|
|
1412
|
+
const codes = [];
|
|
1121
1413
|
for (let i = 0; i < args.length; i++) {
|
|
1122
1414
|
const a = args[i];
|
|
1123
1415
|
if (a == null) {
|
|
1416
|
+
if (codes.length > 0) break;
|
|
1124
1417
|
positionals2++;
|
|
1125
1418
|
selectedProgram = true;
|
|
1126
1419
|
continue;
|
|
1127
1420
|
}
|
|
1128
1421
|
if (!selectedProgram && isInlineCodeFlag(interp, a)) {
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1422
|
+
if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
|
|
1423
|
+
const payload = args[i + 1];
|
|
1424
|
+
if (payload == null) return { interp, code: [null], source: "variable" };
|
|
1425
|
+
return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
|
|
1426
|
+
}
|
|
1427
|
+
const attached = attachedCode(interp, a);
|
|
1428
|
+
if (attached !== null) {
|
|
1429
|
+
codes.push(attached);
|
|
1430
|
+
} else {
|
|
1431
|
+
codes.push(args[i + 1] ?? null);
|
|
1432
|
+
i++;
|
|
1433
|
+
}
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (codes.length > 0) {
|
|
1437
|
+
if (a.startsWith("-")) continue;
|
|
1438
|
+
break;
|
|
1133
1439
|
}
|
|
1134
1440
|
if (a === "-m") {
|
|
1135
1441
|
selectedProgram = true;
|
|
1136
1442
|
continue;
|
|
1137
1443
|
}
|
|
1138
|
-
if (a === "-" && !selectedProgram) return
|
|
1444
|
+
if (a === "-" && !selectedProgram) return fromStdin();
|
|
1139
1445
|
if (!a.startsWith("-")) {
|
|
1140
1446
|
positionals2++;
|
|
1141
1447
|
selectedProgram = true;
|
|
1142
1448
|
}
|
|
1143
1449
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1450
|
+
if (codes.length > 0) {
|
|
1451
|
+
return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
|
|
1452
|
+
}
|
|
1146
1453
|
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
1147
|
-
if (!/^(bash|sh|zsh)$/i.test(interp)) return
|
|
1454
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
|
|
1148
1455
|
}
|
|
1149
|
-
return
|
|
1456
|
+
return null;
|
|
1150
1457
|
}
|
|
1151
1458
|
function shellPayloadNeedsReview(payload) {
|
|
1152
1459
|
if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
|
|
@@ -1179,7 +1486,7 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1179
1486
|
payload
|
|
1180
1487
|
))
|
|
1181
1488
|
review = true;
|
|
1182
|
-
if (!review &&
|
|
1489
|
+
if (!review && inlineExecReview(payload) !== null) review = true;
|
|
1183
1490
|
} catch {
|
|
1184
1491
|
review = true;
|
|
1185
1492
|
} finally {
|
|
@@ -1187,35 +1494,77 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1187
1494
|
}
|
|
1188
1495
|
return review;
|
|
1189
1496
|
}
|
|
1190
|
-
function
|
|
1497
|
+
function inlineExecCalls(command) {
|
|
1191
1498
|
const f = parseShared(command);
|
|
1192
|
-
if (f === PARSE_FAIL)
|
|
1193
|
-
|
|
1194
|
-
command
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
let found = false;
|
|
1499
|
+
if (f === PARSE_FAIL) return PARSE_FAIL;
|
|
1500
|
+
const calls = [];
|
|
1198
1501
|
try {
|
|
1199
1502
|
syntax.Walk(f, (node) => {
|
|
1200
|
-
if (!node ||
|
|
1503
|
+
if (!node || calls.length >= MAX_INLINE_CALLS) return false;
|
|
1201
1504
|
const n = node;
|
|
1202
1505
|
const t = syntax.NodeType(n);
|
|
1203
1506
|
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
return false;
|
|
1207
|
-
}
|
|
1507
|
+
const call = inlineExecCall(n.Y, true);
|
|
1508
|
+
if (call) calls.push(call);
|
|
1208
1509
|
}
|
|
1209
|
-
if (t === "Stmt"
|
|
1210
|
-
|
|
1211
|
-
|
|
1510
|
+
if (t === "Stmt") {
|
|
1511
|
+
const call = inlineExecCall(n, false);
|
|
1512
|
+
if (call) calls.push(call);
|
|
1212
1513
|
}
|
|
1213
1514
|
return true;
|
|
1214
1515
|
});
|
|
1215
1516
|
} catch {
|
|
1216
|
-
return found;
|
|
1217
1517
|
}
|
|
1218
|
-
return
|
|
1518
|
+
return calls;
|
|
1519
|
+
}
|
|
1520
|
+
function detectInlineExec(command) {
|
|
1521
|
+
const calls = inlineExecCalls(command);
|
|
1522
|
+
if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
|
|
1523
|
+
return calls.length > 0;
|
|
1524
|
+
}
|
|
1525
|
+
function isProtectedCodePath(p) {
|
|
1526
|
+
if (matchSensitivePath(p) !== null) return true;
|
|
1527
|
+
return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
|
|
1528
|
+
}
|
|
1529
|
+
function inlineExecDescription(r) {
|
|
1530
|
+
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.";
|
|
1531
|
+
return `${lead} Review the full script below before allowing it to execute.`;
|
|
1532
|
+
}
|
|
1533
|
+
function inlineExecReason(r) {
|
|
1534
|
+
const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
|
|
1535
|
+
return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
|
|
1536
|
+
}
|
|
1537
|
+
function inlineExecReview(command) {
|
|
1538
|
+
const calls = inlineExecCalls(command);
|
|
1539
|
+
if (calls === PARSE_FAIL) {
|
|
1540
|
+
return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
|
|
1541
|
+
}
|
|
1542
|
+
let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
|
|
1543
|
+
for (const call of calls) {
|
|
1544
|
+
const language = inlineLanguageOf(call.interp);
|
|
1545
|
+
for (const code of call.code) {
|
|
1546
|
+
let finding = null;
|
|
1547
|
+
if (code === null) {
|
|
1548
|
+
finding = {
|
|
1549
|
+
outcome: "unreadable",
|
|
1550
|
+
interp: call.interp,
|
|
1551
|
+
what: UNREADABLE_SOURCE[call.source ?? "variable"]
|
|
1552
|
+
};
|
|
1553
|
+
} else if (language === null) {
|
|
1554
|
+
finding = {
|
|
1555
|
+
outcome: "unreadable",
|
|
1556
|
+
interp: call.interp,
|
|
1557
|
+
what: `is ${call.interp} code, which node9 does not read`
|
|
1558
|
+
};
|
|
1559
|
+
} else {
|
|
1560
|
+
const judged = judgeInlineCode(language, code, isProtectedCodePath);
|
|
1561
|
+
if (judged) finding = { ...judged, interp: call.interp };
|
|
1562
|
+
}
|
|
1563
|
+
if (finding?.outcome === "dangerous") return finding;
|
|
1564
|
+
if (finding && !unreadable) unreadable = finding;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return unreadable;
|
|
1219
1568
|
}
|
|
1220
1569
|
function analyzeChmod777(command) {
|
|
1221
1570
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
@@ -2464,40 +2813,6 @@ function analyzeShellCommand(command) {
|
|
|
2464
2813
|
}
|
|
2465
2814
|
return { actions, paths, allTokens };
|
|
2466
2815
|
}
|
|
2467
|
-
function validateRegex(pattern) {
|
|
2468
|
-
if (!pattern) return "Pattern is required";
|
|
2469
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2470
|
-
try {
|
|
2471
|
-
new RegExp(pattern);
|
|
2472
|
-
} catch (e) {
|
|
2473
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2474
|
-
}
|
|
2475
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2476
|
-
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2477
|
-
return null;
|
|
2478
|
-
}
|
|
2479
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2480
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2481
|
-
const key = `${pattern}\0${flags}`;
|
|
2482
|
-
if (regexCache.has(key)) {
|
|
2483
|
-
const cached = regexCache.get(key);
|
|
2484
|
-
regexCache.delete(key);
|
|
2485
|
-
regexCache.set(key, cached);
|
|
2486
|
-
return cached;
|
|
2487
|
-
}
|
|
2488
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2489
|
-
try {
|
|
2490
|
-
const re = new RegExp(pattern, flags);
|
|
2491
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2492
|
-
const oldest = regexCache.keys().next().value;
|
|
2493
|
-
if (oldest) regexCache.delete(oldest);
|
|
2494
|
-
}
|
|
2495
|
-
regexCache.set(key, re);
|
|
2496
|
-
return re;
|
|
2497
|
-
} catch {
|
|
2498
|
-
return null;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
2816
|
function matchesPattern(text, patterns) {
|
|
2502
2817
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2503
2818
|
if (p.length === 0) return false;
|
|
@@ -2508,9 +2823,21 @@ function matchesPattern(text, patterns) {
|
|
|
2508
2823
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2509
2824
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2510
2825
|
}
|
|
2511
|
-
function
|
|
2826
|
+
function mcpServerFromToolName(toolName) {
|
|
2827
|
+
const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
2828
|
+
return match?.[1];
|
|
2829
|
+
}
|
|
2830
|
+
function ruleServerMatches(rule, serverLabel) {
|
|
2831
|
+
const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
|
|
2832
|
+
if (rule.server) {
|
|
2833
|
+
if (!label2 || !matchesPattern(label2, rule.server)) return false;
|
|
2834
|
+
}
|
|
2835
|
+
if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
|
|
2836
|
+
return true;
|
|
2837
|
+
}
|
|
2838
|
+
function getNestedValue(obj, path28) {
|
|
2512
2839
|
if (!obj || typeof obj !== "object") return null;
|
|
2513
|
-
const segments2 =
|
|
2840
|
+
const segments2 = path28.split(".");
|
|
2514
2841
|
for (const seg of segments2) {
|
|
2515
2842
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2516
2843
|
}
|
|
@@ -2530,23 +2857,26 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2530
2857
|
return val;
|
|
2531
2858
|
};
|
|
2532
2859
|
const readingsCache = /* @__PURE__ */ new Map();
|
|
2533
|
-
const resolveFieldReadings = (field) => {
|
|
2534
|
-
const
|
|
2860
|
+
const resolveFieldReadings = (field, reading) => {
|
|
2861
|
+
const access = reading === "access" && field === "command";
|
|
2862
|
+
const key = access ? `${field}\0access` : field;
|
|
2863
|
+
const cached = readingsCache.get(key);
|
|
2535
2864
|
if (cached) return cached;
|
|
2536
2865
|
const primary = resolveField(field);
|
|
2537
2866
|
if (primary === null) {
|
|
2538
|
-
readingsCache.set(
|
|
2867
|
+
readingsCache.set(key, []);
|
|
2539
2868
|
return [];
|
|
2540
2869
|
}
|
|
2541
2870
|
let out = [primary];
|
|
2542
2871
|
if (field === "command") {
|
|
2543
2872
|
const raw = getNestedValue(args, field);
|
|
2544
2873
|
if (typeof raw === "string") {
|
|
2545
|
-
const
|
|
2546
|
-
|
|
2874
|
+
const text = access ? fileAccessView(raw) : raw;
|
|
2875
|
+
const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
|
|
2876
|
+
out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
2547
2877
|
}
|
|
2548
2878
|
}
|
|
2549
|
-
readingsCache.set(
|
|
2879
|
+
readingsCache.set(key, out);
|
|
2550
2880
|
return out;
|
|
2551
2881
|
};
|
|
2552
2882
|
const results = rule.conditions.map((cond) => {
|
|
@@ -2564,14 +2894,14 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2564
2894
|
if (val === null || !cond.value) return false;
|
|
2565
2895
|
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2566
2896
|
if (!reM) return false;
|
|
2567
|
-
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
2897
|
+
return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
|
|
2568
2898
|
}
|
|
2569
2899
|
case "notMatches": {
|
|
2570
2900
|
if (!cond.value) return false;
|
|
2571
2901
|
if (val === null) return true;
|
|
2572
2902
|
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2573
2903
|
if (!reN) return false;
|
|
2574
|
-
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
2904
|
+
return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
|
|
2575
2905
|
}
|
|
2576
2906
|
case "matchesGlob":
|
|
2577
2907
|
return val !== null && cond.value ? import_picomatch2.default.isMatch(val, cond.value) : false;
|
|
@@ -3413,35 +3743,6 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
3413
3743
|
}
|
|
3414
3744
|
return out;
|
|
3415
3745
|
}
|
|
3416
|
-
function isAgentInstructionPath(absPath, root) {
|
|
3417
|
-
const r = root.replace(/\/$/, "");
|
|
3418
|
-
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
3419
|
-
const rel = absPath.slice(r.length + 1);
|
|
3420
|
-
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
3421
|
-
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
3422
|
-
}
|
|
3423
|
-
function isInside(root, p) {
|
|
3424
|
-
const r = root.replace(/\/$/, "") || "/";
|
|
3425
|
-
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
3426
|
-
}
|
|
3427
|
-
function isPasswordHashPath(p) {
|
|
3428
|
-
return PASSWORD_HASH_FILES.includes(p);
|
|
3429
|
-
}
|
|
3430
|
-
function isLegacySystemPath(p) {
|
|
3431
|
-
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
3432
|
-
}
|
|
3433
|
-
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
3434
|
-
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
3435
|
-
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
3436
|
-
const rooted = p.startsWith("/");
|
|
3437
|
-
const parts = [];
|
|
3438
|
-
for (const part of p.split("/")) {
|
|
3439
|
-
if (!part || part === ".") continue;
|
|
3440
|
-
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
3441
|
-
else if (part !== ".." || !rooted) parts.push(part);
|
|
3442
|
-
}
|
|
3443
|
-
return (rooted ? "/" : "") + parts.join("/");
|
|
3444
|
-
}
|
|
3445
3746
|
function incompleteFileVerdict(operation = "write") {
|
|
3446
3747
|
return {
|
|
3447
3748
|
decision: "review",
|
|
@@ -3493,8 +3794,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
3493
3794
|
}
|
|
3494
3795
|
return out;
|
|
3495
3796
|
}
|
|
3496
|
-
function within(
|
|
3497
|
-
return
|
|
3797
|
+
function within(path28, root) {
|
|
3798
|
+
return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
|
|
3498
3799
|
}
|
|
3499
3800
|
function hits(access, paths, directories, context) {
|
|
3500
3801
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -4302,14 +4603,14 @@ function sensitivePathMatch(originalPath) {
|
|
|
4302
4603
|
}
|
|
4303
4604
|
function assertBuiltinPatternsAreSafe() {
|
|
4304
4605
|
for (const p of DLP_PATTERNS) {
|
|
4305
|
-
if (!(0,
|
|
4606
|
+
if (!(0, import_safe_regex22.default)(p.regex.source)) {
|
|
4306
4607
|
throw new Error(
|
|
4307
4608
|
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
4308
4609
|
);
|
|
4309
4610
|
}
|
|
4310
4611
|
}
|
|
4311
4612
|
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
4312
|
-
if (!(0,
|
|
4613
|
+
if (!(0, import_safe_regex22.default)(re.source)) {
|
|
4313
4614
|
throw new Error(
|
|
4314
4615
|
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
4315
4616
|
);
|
|
@@ -5433,9 +5734,9 @@ function bareToolName(toolName) {
|
|
|
5433
5734
|
const parts = toolName.split("__");
|
|
5434
5735
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
5435
5736
|
}
|
|
5436
|
-
function valuesAt(args,
|
|
5737
|
+
function valuesAt(args, path28) {
|
|
5437
5738
|
let cursors = [args];
|
|
5438
|
-
for (const rawSegment of
|
|
5739
|
+
for (const rawSegment of path28.split(".")) {
|
|
5439
5740
|
const isArray = rawSegment.endsWith("[]");
|
|
5440
5741
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
5441
5742
|
const next = [];
|
|
@@ -5466,8 +5767,8 @@ function extractToolDestinations(toolName, args) {
|
|
|
5466
5767
|
try {
|
|
5467
5768
|
const out = [];
|
|
5468
5769
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
|
|
5469
|
-
for (const
|
|
5470
|
-
for (const value of valuesAt(args,
|
|
5770
|
+
for (const path28 of paths) {
|
|
5771
|
+
for (const value of valuesAt(args, path28)) {
|
|
5471
5772
|
const host2 = hostOf(value);
|
|
5472
5773
|
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
5473
5774
|
}
|
|
@@ -5493,8 +5794,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5493
5794
|
try {
|
|
5494
5795
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
5495
5796
|
if (!paths) return null;
|
|
5496
|
-
for (const
|
|
5497
|
-
for (const value of valuesAt(args,
|
|
5797
|
+
for (const path28 of paths) {
|
|
5798
|
+
for (const value of valuesAt(args, path28)) {
|
|
5498
5799
|
const host2 = hostOf(value);
|
|
5499
5800
|
if (!host2) continue;
|
|
5500
5801
|
const m = classifySsrf(host2);
|
|
@@ -5503,7 +5804,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5503
5804
|
const decision = ssrfFloorDecision(m, written, opts);
|
|
5504
5805
|
if (decision === null) continue;
|
|
5505
5806
|
const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
|
|
5506
|
-
return { ...m, argPath:
|
|
5807
|
+
return { ...m, argPath: path28, host: host2, reason, decision };
|
|
5507
5808
|
}
|
|
5508
5809
|
}
|
|
5509
5810
|
return null;
|
|
@@ -5883,11 +6184,15 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5883
6184
|
};
|
|
5884
6185
|
const ruleCheck = (rule) => {
|
|
5885
6186
|
if (rule.verdict === "allow") return void 0;
|
|
5886
|
-
if (!rule.builtin
|
|
6187
|
+
if (!rule.builtin) return void 0;
|
|
5887
6188
|
const id = checkIdForRule(rule.name);
|
|
5888
6189
|
if (!id) return void 0;
|
|
5889
6190
|
const def = getCheck(id);
|
|
5890
6191
|
if (!def) return void 0;
|
|
6192
|
+
if (rule.pinned) {
|
|
6193
|
+
const explicit = config.policy.checks?.[id];
|
|
6194
|
+
return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
|
|
6195
|
+
}
|
|
5891
6196
|
if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
|
|
5892
6197
|
if (def.pack) {
|
|
5893
6198
|
const explicit = config.policy.checks?.[id];
|
|
@@ -5895,8 +6200,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5895
6200
|
}
|
|
5896
6201
|
return { id, value: cv(id) };
|
|
5897
6202
|
};
|
|
6203
|
+
const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
|
|
5898
6204
|
const matches = config.policy.smartRules.filter((rule) => {
|
|
5899
6205
|
if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
|
|
6206
|
+
if (!ruleServerMatches(rule, serverLabel)) return false;
|
|
5900
6207
|
if (astSuppressed(rule)) return false;
|
|
5901
6208
|
if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
|
|
5902
6209
|
if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
|
|
@@ -5973,11 +6280,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5973
6280
|
if (v?.logged) noteLogged(v);
|
|
5974
6281
|
else if (v) candidates.push(v);
|
|
5975
6282
|
};
|
|
5976
|
-
|
|
6283
|
+
const inline = config.settings.mode === "strict" ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
|
|
6284
|
+
if (inline !== null) {
|
|
5977
6285
|
consider("commands.inline-exec", {
|
|
5978
6286
|
decision: "review",
|
|
5979
6287
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
5980
|
-
|
|
6288
|
+
...inline ? { reason: inlineExecReason(inline) } : {},
|
|
6289
|
+
ruleDescription: inlineExecDescription(inline),
|
|
5981
6290
|
tier: 3,
|
|
5982
6291
|
checkId: "commands.inline-exec"
|
|
5983
6292
|
});
|
|
@@ -7067,7 +7376,7 @@ function matchCanaryArgs(args, values) {
|
|
|
7067
7376
|
return null;
|
|
7068
7377
|
}
|
|
7069
7378
|
}
|
|
7070
|
-
var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh,
|
|
7379
|
+
var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_mvdan_sh2, import_safe_regex22, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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, 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;
|
|
7071
7380
|
var init_dist = __esm({
|
|
7072
7381
|
"packages/policy-engine/dist/index.mjs"() {
|
|
7073
7382
|
"use strict";
|
|
@@ -7075,15 +7384,17 @@ var init_dist = __esm({
|
|
|
7075
7384
|
import_safe_regex2 = __toESM(require("safe-regex2"), 1);
|
|
7076
7385
|
import_picomatch2 = __toESM(require("picomatch"), 1);
|
|
7077
7386
|
import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
|
|
7078
|
-
import_safe_regex22 = __toESM(require("safe-regex2"), 1);
|
|
7079
7387
|
import_mvdan_sh2 = __toESM(require("mvdan-sh"), 1);
|
|
7080
|
-
|
|
7388
|
+
import_safe_regex22 = __toESM(require("safe-regex2"), 1);
|
|
7081
7389
|
import_crypto2 = require("crypto");
|
|
7082
7390
|
import_crypto3 = require("crypto");
|
|
7083
7391
|
import_mvdan_sh3 = __toESM(require("mvdan-sh"), 1);
|
|
7084
7392
|
import_crypto4 = require("crypto");
|
|
7085
7393
|
import_mvdan_sh4 = __toESM(require("mvdan-sh"), 1);
|
|
7086
7394
|
import_crypto5 = __toESM(require("crypto"), 1);
|
|
7395
|
+
MAX_REGEX_LENGTH = 256;
|
|
7396
|
+
REGEX_CACHE_MAX = 500;
|
|
7397
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
7087
7398
|
aws_default = {
|
|
7088
7399
|
name: "aws",
|
|
7089
7400
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -7385,81 +7696,10 @@ var init_dist = __esm({
|
|
|
7385
7696
|
],
|
|
7386
7697
|
dangerousWords: ["wipefs"]
|
|
7387
7698
|
};
|
|
7388
|
-
|
|
7389
|
-
name: "
|
|
7390
|
-
description: "
|
|
7391
|
-
aliases: ["
|
|
7392
|
-
smartRules: [
|
|
7393
|
-
{
|
|
7394
|
-
name: "shield:finance:review-refund",
|
|
7395
|
-
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
7396
|
-
conditions: [],
|
|
7397
|
-
verdict: "review",
|
|
7398
|
-
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
7399
|
-
},
|
|
7400
|
-
{
|
|
7401
|
-
name: "shield:finance:review-payout",
|
|
7402
|
-
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
7403
|
-
conditions: [],
|
|
7404
|
-
verdict: "review",
|
|
7405
|
-
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
7406
|
-
},
|
|
7407
|
-
{
|
|
7408
|
-
name: "shield:finance:review-transfer",
|
|
7409
|
-
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}",
|
|
7410
|
-
conditions: [],
|
|
7411
|
-
verdict: "review",
|
|
7412
|
-
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
7413
|
-
},
|
|
7414
|
-
{
|
|
7415
|
-
name: "shield:finance:review-charge",
|
|
7416
|
-
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}",
|
|
7417
|
-
conditions: [],
|
|
7418
|
-
verdict: "review",
|
|
7419
|
-
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
7420
|
-
},
|
|
7421
|
-
{
|
|
7422
|
-
name: "shield:finance:review-payment-link",
|
|
7423
|
-
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
7424
|
-
conditions: [],
|
|
7425
|
-
verdict: "review",
|
|
7426
|
-
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
7427
|
-
},
|
|
7428
|
-
{
|
|
7429
|
-
name: "shield:finance:review-subscription",
|
|
7430
|
-
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
7431
|
-
conditions: [],
|
|
7432
|
-
verdict: "review",
|
|
7433
|
-
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
7434
|
-
},
|
|
7435
|
-
{
|
|
7436
|
-
name: "shield:finance:review-dispute",
|
|
7437
|
-
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
7438
|
-
conditions: [],
|
|
7439
|
-
verdict: "review",
|
|
7440
|
-
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
7441
|
-
},
|
|
7442
|
-
{
|
|
7443
|
-
name: "shield:finance:review-card",
|
|
7444
|
-
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
7445
|
-
conditions: [],
|
|
7446
|
-
verdict: "review",
|
|
7447
|
-
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
7448
|
-
},
|
|
7449
|
-
{
|
|
7450
|
-
name: "shield:finance:review-withdraw",
|
|
7451
|
-
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
7452
|
-
conditions: [],
|
|
7453
|
-
verdict: "review",
|
|
7454
|
-
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
7455
|
-
}
|
|
7456
|
-
],
|
|
7457
|
-
dangerousWords: []
|
|
7458
|
-
};
|
|
7459
|
-
github_default = {
|
|
7460
|
-
name: "github",
|
|
7461
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
7462
|
-
aliases: ["git"],
|
|
7699
|
+
github_default = {
|
|
7700
|
+
name: "github",
|
|
7701
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
7702
|
+
aliases: ["git"],
|
|
7463
7703
|
smartRules: [
|
|
7464
7704
|
{
|
|
7465
7705
|
name: "shield:github:review-delete-branch-remote",
|
|
@@ -7482,56 +7722,6 @@ var init_dist = __esm({
|
|
|
7482
7722
|
],
|
|
7483
7723
|
dangerousWords: []
|
|
7484
7724
|
};
|
|
7485
|
-
identity_default = {
|
|
7486
|
-
name: "identity",
|
|
7487
|
-
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.",
|
|
7488
|
-
aliases: ["access", "iam"],
|
|
7489
|
-
smartRules: [
|
|
7490
|
-
{
|
|
7491
|
-
name: "shield:identity:review-password",
|
|
7492
|
-
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
7493
|
-
conditions: [],
|
|
7494
|
-
verdict: "review",
|
|
7495
|
-
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
7496
|
-
},
|
|
7497
|
-
{
|
|
7498
|
-
name: "shield:identity:review-key",
|
|
7499
|
-
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}",
|
|
7500
|
-
conditions: [],
|
|
7501
|
-
verdict: "review",
|
|
7502
|
-
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
7503
|
-
},
|
|
7504
|
-
{
|
|
7505
|
-
name: "shield:identity:review-access",
|
|
7506
|
-
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}",
|
|
7507
|
-
conditions: [],
|
|
7508
|
-
verdict: "review",
|
|
7509
|
-
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
7510
|
-
},
|
|
7511
|
-
{
|
|
7512
|
-
name: "shield:identity:review-membership",
|
|
7513
|
-
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}",
|
|
7514
|
-
conditions: [],
|
|
7515
|
-
verdict: "review",
|
|
7516
|
-
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
7517
|
-
},
|
|
7518
|
-
{
|
|
7519
|
-
name: "shield:identity:review-user",
|
|
7520
|
-
tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
7521
|
-
conditions: [],
|
|
7522
|
-
verdict: "review",
|
|
7523
|
-
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
7524
|
-
},
|
|
7525
|
-
{
|
|
7526
|
-
name: "shield:identity:review-mfa",
|
|
7527
|
-
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
7528
|
-
conditions: [],
|
|
7529
|
-
verdict: "review",
|
|
7530
|
-
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
7531
|
-
}
|
|
7532
|
-
],
|
|
7533
|
-
dangerousWords: []
|
|
7534
|
-
};
|
|
7535
7725
|
k8s_default = {
|
|
7536
7726
|
name: "k8s",
|
|
7537
7727
|
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
@@ -7784,7 +7974,7 @@ var init_dist = __esm({
|
|
|
7784
7974
|
{
|
|
7785
7975
|
field: "command",
|
|
7786
7976
|
op: "matches",
|
|
7787
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
7977
|
+
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;&|>)<]|$)",
|
|
7788
7978
|
flags: "i"
|
|
7789
7979
|
}
|
|
7790
7980
|
],
|
|
@@ -7798,7 +7988,7 @@ var init_dist = __esm({
|
|
|
7798
7988
|
{
|
|
7799
7989
|
field: "command",
|
|
7800
7990
|
op: "matches",
|
|
7801
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
7991
|
+
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)",
|
|
7802
7992
|
flags: "i"
|
|
7803
7993
|
}
|
|
7804
7994
|
],
|
|
@@ -7848,18 +8038,218 @@ var init_dist = __esm({
|
|
|
7848
8038
|
reason: "Reading .env files is blocked by project-jail shield"
|
|
7849
8039
|
},
|
|
7850
8040
|
{
|
|
7851
|
-
name: "shield:project-jail:review-read-credentials-any-tool",
|
|
7852
|
-
tool: "*",
|
|
7853
|
-
conditions: [
|
|
7854
|
-
{
|
|
7855
|
-
field: "file_path",
|
|
7856
|
-
op: "matches",
|
|
7857
|
-
value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
|
|
7858
|
-
flags: "i"
|
|
7859
|
-
}
|
|
7860
|
-
],
|
|
8041
|
+
name: "shield:project-jail:review-read-credentials-any-tool",
|
|
8042
|
+
tool: "*",
|
|
8043
|
+
conditions: [
|
|
8044
|
+
{
|
|
8045
|
+
field: "file_path",
|
|
8046
|
+
op: "matches",
|
|
8047
|
+
value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
|
|
8048
|
+
flags: "i"
|
|
8049
|
+
}
|
|
8050
|
+
],
|
|
8051
|
+
verdict: "review",
|
|
8052
|
+
reason: "Reading credential files requires approval (project-jail shield)"
|
|
8053
|
+
}
|
|
8054
|
+
],
|
|
8055
|
+
dangerousWords: []
|
|
8056
|
+
};
|
|
8057
|
+
redis_default = {
|
|
8058
|
+
name: "redis",
|
|
8059
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
8060
|
+
aliases: [],
|
|
8061
|
+
_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.",
|
|
8062
|
+
smartRules: [
|
|
8063
|
+
{
|
|
8064
|
+
name: "shield:redis:block-flushall",
|
|
8065
|
+
tool: "*",
|
|
8066
|
+
conditions: [
|
|
8067
|
+
{
|
|
8068
|
+
field: "command",
|
|
8069
|
+
op: "matches",
|
|
8070
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
8071
|
+
flags: "i"
|
|
8072
|
+
}
|
|
8073
|
+
],
|
|
8074
|
+
verdict: "block",
|
|
8075
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
8076
|
+
},
|
|
8077
|
+
{
|
|
8078
|
+
name: "shield:redis:block-flushdb",
|
|
8079
|
+
tool: "*",
|
|
8080
|
+
conditions: [
|
|
8081
|
+
{
|
|
8082
|
+
field: "command",
|
|
8083
|
+
op: "matches",
|
|
8084
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
8085
|
+
flags: "i"
|
|
8086
|
+
}
|
|
8087
|
+
],
|
|
8088
|
+
verdict: "block",
|
|
8089
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
8090
|
+
},
|
|
8091
|
+
{
|
|
8092
|
+
name: "shield:redis:block-config-resetstat",
|
|
8093
|
+
tool: "*",
|
|
8094
|
+
conditions: [
|
|
8095
|
+
{
|
|
8096
|
+
field: "command",
|
|
8097
|
+
op: "matches",
|
|
8098
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
8099
|
+
flags: "i"
|
|
8100
|
+
}
|
|
8101
|
+
],
|
|
8102
|
+
verdict: "block",
|
|
8103
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
8104
|
+
},
|
|
8105
|
+
{
|
|
8106
|
+
name: "shield:redis:review-config-set",
|
|
8107
|
+
tool: "*",
|
|
8108
|
+
conditions: [
|
|
8109
|
+
{
|
|
8110
|
+
field: "command",
|
|
8111
|
+
op: "matches",
|
|
8112
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
8113
|
+
flags: "i"
|
|
8114
|
+
}
|
|
8115
|
+
],
|
|
8116
|
+
verdict: "review",
|
|
8117
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
8118
|
+
},
|
|
8119
|
+
{
|
|
8120
|
+
name: "shield:redis:review-del-wildcard",
|
|
8121
|
+
tool: "*",
|
|
8122
|
+
conditions: [
|
|
8123
|
+
{
|
|
8124
|
+
field: "command",
|
|
8125
|
+
op: "matches",
|
|
8126
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
8127
|
+
flags: "i"
|
|
8128
|
+
}
|
|
8129
|
+
],
|
|
8130
|
+
verdict: "review",
|
|
8131
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
8132
|
+
}
|
|
8133
|
+
],
|
|
8134
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
8135
|
+
};
|
|
8136
|
+
finance_default = {
|
|
8137
|
+
name: "finance",
|
|
8138
|
+
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.",
|
|
8139
|
+
aliases: ["payments", "money"],
|
|
8140
|
+
smartRules: [
|
|
8141
|
+
{
|
|
8142
|
+
name: "shield:finance:review-refund",
|
|
8143
|
+
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
8144
|
+
conditions: [],
|
|
8145
|
+
verdict: "review",
|
|
8146
|
+
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
8147
|
+
},
|
|
8148
|
+
{
|
|
8149
|
+
name: "shield:finance:review-payout",
|
|
8150
|
+
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
8151
|
+
conditions: [],
|
|
8152
|
+
verdict: "review",
|
|
8153
|
+
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
8154
|
+
},
|
|
8155
|
+
{
|
|
8156
|
+
name: "shield:finance:review-transfer",
|
|
8157
|
+
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
|
|
8158
|
+
conditions: [],
|
|
8159
|
+
verdict: "review",
|
|
8160
|
+
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
8161
|
+
},
|
|
8162
|
+
{
|
|
8163
|
+
name: "shield:finance:review-charge",
|
|
8164
|
+
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}",
|
|
8165
|
+
conditions: [],
|
|
8166
|
+
verdict: "review",
|
|
8167
|
+
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
8168
|
+
},
|
|
8169
|
+
{
|
|
8170
|
+
name: "shield:finance:review-payment-link",
|
|
8171
|
+
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
8172
|
+
conditions: [],
|
|
8173
|
+
verdict: "review",
|
|
8174
|
+
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
8175
|
+
},
|
|
8176
|
+
{
|
|
8177
|
+
name: "shield:finance:review-subscription",
|
|
8178
|
+
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
8179
|
+
conditions: [],
|
|
8180
|
+
verdict: "review",
|
|
8181
|
+
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
8182
|
+
},
|
|
8183
|
+
{
|
|
8184
|
+
name: "shield:finance:review-dispute",
|
|
8185
|
+
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
8186
|
+
conditions: [],
|
|
8187
|
+
verdict: "review",
|
|
8188
|
+
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
8189
|
+
},
|
|
8190
|
+
{
|
|
8191
|
+
name: "shield:finance:review-card",
|
|
8192
|
+
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
8193
|
+
conditions: [],
|
|
8194
|
+
verdict: "review",
|
|
8195
|
+
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
8196
|
+
},
|
|
8197
|
+
{
|
|
8198
|
+
name: "shield:finance:review-withdraw",
|
|
8199
|
+
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
8200
|
+
conditions: [],
|
|
8201
|
+
verdict: "review",
|
|
8202
|
+
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
8203
|
+
}
|
|
8204
|
+
],
|
|
8205
|
+
dangerousWords: []
|
|
8206
|
+
};
|
|
8207
|
+
identity_default = {
|
|
8208
|
+
name: "identity",
|
|
8209
|
+
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.",
|
|
8210
|
+
aliases: ["access", "iam"],
|
|
8211
|
+
smartRules: [
|
|
8212
|
+
{
|
|
8213
|
+
name: "shield:identity:review-password",
|
|
8214
|
+
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
8215
|
+
conditions: [],
|
|
8216
|
+
verdict: "review",
|
|
8217
|
+
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
8218
|
+
},
|
|
8219
|
+
{
|
|
8220
|
+
name: "shield:identity:review-key",
|
|
8221
|
+
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}",
|
|
8222
|
+
conditions: [],
|
|
8223
|
+
verdict: "review",
|
|
8224
|
+
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
8225
|
+
},
|
|
8226
|
+
{
|
|
8227
|
+
name: "shield:identity:review-access",
|
|
8228
|
+
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}",
|
|
8229
|
+
conditions: [],
|
|
8230
|
+
verdict: "review",
|
|
8231
|
+
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
8232
|
+
},
|
|
8233
|
+
{
|
|
8234
|
+
name: "shield:identity:review-membership",
|
|
8235
|
+
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}",
|
|
8236
|
+
conditions: [],
|
|
8237
|
+
verdict: "review",
|
|
8238
|
+
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
8239
|
+
},
|
|
8240
|
+
{
|
|
8241
|
+
name: "shield:identity:review-user",
|
|
8242
|
+
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}",
|
|
8243
|
+
conditions: [],
|
|
7861
8244
|
verdict: "review",
|
|
7862
|
-
reason: "
|
|
8245
|
+
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
8246
|
+
},
|
|
8247
|
+
{
|
|
8248
|
+
name: "shield:identity:review-mfa",
|
|
8249
|
+
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
8250
|
+
conditions: [],
|
|
8251
|
+
verdict: "review",
|
|
8252
|
+
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
7863
8253
|
}
|
|
7864
8254
|
],
|
|
7865
8255
|
dangerousWords: []
|
|
@@ -8033,103 +8423,510 @@ var init_dist = __esm({
|
|
|
8033
8423
|
],
|
|
8034
8424
|
dangerousWords: []
|
|
8035
8425
|
};
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
{
|
|
8061
|
-
field: "command",
|
|
8062
|
-
op: "matches",
|
|
8063
|
-
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
8064
|
-
flags: "i"
|
|
8065
|
-
}
|
|
8066
|
-
],
|
|
8067
|
-
verdict: "block",
|
|
8068
|
-
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
8069
|
-
},
|
|
8070
|
-
{
|
|
8071
|
-
name: "shield:redis:block-config-resetstat",
|
|
8072
|
-
tool: "*",
|
|
8073
|
-
conditions: [
|
|
8074
|
-
{
|
|
8075
|
-
field: "command",
|
|
8076
|
-
op: "matches",
|
|
8077
|
-
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
8078
|
-
flags: "i"
|
|
8079
|
-
}
|
|
8080
|
-
],
|
|
8081
|
-
verdict: "block",
|
|
8082
|
-
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
8083
|
-
},
|
|
8084
|
-
{
|
|
8085
|
-
name: "shield:redis:review-config-set",
|
|
8086
|
-
tool: "*",
|
|
8087
|
-
conditions: [
|
|
8088
|
-
{
|
|
8089
|
-
field: "command",
|
|
8090
|
-
op: "matches",
|
|
8091
|
-
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
8092
|
-
flags: "i"
|
|
8093
|
-
}
|
|
8094
|
-
],
|
|
8095
|
-
verdict: "review",
|
|
8096
|
-
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
8097
|
-
},
|
|
8098
|
-
{
|
|
8099
|
-
name: "shield:redis:review-del-wildcard",
|
|
8100
|
-
tool: "*",
|
|
8101
|
-
conditions: [
|
|
8102
|
-
{
|
|
8103
|
-
field: "command",
|
|
8104
|
-
op: "matches",
|
|
8105
|
-
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
8106
|
-
flags: "i"
|
|
8107
|
-
}
|
|
8108
|
-
],
|
|
8109
|
-
verdict: "review",
|
|
8110
|
-
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
8111
|
-
}
|
|
8112
|
-
],
|
|
8113
|
-
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
8426
|
+
ACTION_TEMPLATES = {
|
|
8427
|
+
finance: finance_default.smartRules,
|
|
8428
|
+
identity: identity_default.smartRules,
|
|
8429
|
+
publishing: publishing_default.smartRules
|
|
8430
|
+
};
|
|
8431
|
+
ACTION_CHECKS = {
|
|
8432
|
+
finance: { checkId: "actions.money", slug: "money", label: "Money check" },
|
|
8433
|
+
identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
|
|
8434
|
+
publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
|
|
8435
|
+
};
|
|
8436
|
+
GAP = "[^;&|]{0,200}?";
|
|
8437
|
+
prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
|
|
8438
|
+
httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
|
|
8439
|
+
httpWrite = (methods) => ({
|
|
8440
|
+
field: "command",
|
|
8441
|
+
op: "matches",
|
|
8442
|
+
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)`
|
|
8443
|
+
});
|
|
8444
|
+
HTTP_WRITE = httpWrite("POST|DELETE");
|
|
8445
|
+
HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
|
|
8446
|
+
NOT_STRIPE_TEST = {
|
|
8447
|
+
field: "command",
|
|
8448
|
+
op: "notMatches",
|
|
8449
|
+
value: "\\b[sr]k_test_"
|
|
8114
8450
|
};
|
|
8451
|
+
postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
|
|
8452
|
+
STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
|
|
8453
|
+
APP_SHIELD_SPECS = [
|
|
8454
|
+
{
|
|
8455
|
+
name: "stripe",
|
|
8456
|
+
label: "Stripe",
|
|
8457
|
+
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.",
|
|
8458
|
+
template: "finance",
|
|
8459
|
+
server: "*stripe*",
|
|
8460
|
+
doors: [
|
|
8461
|
+
{
|
|
8462
|
+
name: "shield:stripe:review-cli",
|
|
8463
|
+
tool: "bash",
|
|
8464
|
+
conditions: [
|
|
8465
|
+
{
|
|
8466
|
+
field: "command",
|
|
8467
|
+
op: "matches",
|
|
8468
|
+
value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
|
|
8469
|
+
flags: "i"
|
|
8470
|
+
},
|
|
8471
|
+
{
|
|
8472
|
+
field: "command",
|
|
8473
|
+
op: "matches",
|
|
8474
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8475
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8476
|
+
}
|
|
8477
|
+
],
|
|
8478
|
+
conditionMode: "all",
|
|
8479
|
+
verdict: "review",
|
|
8480
|
+
reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
|
|
8481
|
+
},
|
|
8482
|
+
{
|
|
8483
|
+
name: "shield:stripe:review-cli-request",
|
|
8484
|
+
tool: "bash",
|
|
8485
|
+
conditions: [
|
|
8486
|
+
{
|
|
8487
|
+
field: "command",
|
|
8488
|
+
op: "matches",
|
|
8489
|
+
value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
|
|
8490
|
+
flags: "i"
|
|
8491
|
+
},
|
|
8492
|
+
{
|
|
8493
|
+
field: "command",
|
|
8494
|
+
op: "matches",
|
|
8495
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8496
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8497
|
+
}
|
|
8498
|
+
],
|
|
8499
|
+
conditionMode: "all",
|
|
8500
|
+
verdict: "review",
|
|
8501
|
+
reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
|
|
8502
|
+
},
|
|
8503
|
+
{
|
|
8504
|
+
name: "shield:stripe:review-api",
|
|
8505
|
+
tool: "bash",
|
|
8506
|
+
conditions: [
|
|
8507
|
+
{
|
|
8508
|
+
field: "command",
|
|
8509
|
+
op: "matches",
|
|
8510
|
+
value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
|
|
8511
|
+
flags: "i"
|
|
8512
|
+
},
|
|
8513
|
+
HTTP_WRITE,
|
|
8514
|
+
NOT_STRIPE_TEST
|
|
8515
|
+
],
|
|
8516
|
+
conditionMode: "all",
|
|
8517
|
+
verdict: "review",
|
|
8518
|
+
reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
|
|
8519
|
+
},
|
|
8520
|
+
{
|
|
8521
|
+
name: "shield:stripe:review-code",
|
|
8522
|
+
tool: "bash",
|
|
8523
|
+
conditions: [
|
|
8524
|
+
{
|
|
8525
|
+
field: "command",
|
|
8526
|
+
op: "matches",
|
|
8527
|
+
// Python: stripe.Refund.create(...)
|
|
8528
|
+
value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
|
|
8529
|
+
},
|
|
8530
|
+
NOT_STRIPE_TEST
|
|
8531
|
+
],
|
|
8532
|
+
conditionMode: "all",
|
|
8533
|
+
verdict: "review",
|
|
8534
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8535
|
+
},
|
|
8536
|
+
{
|
|
8537
|
+
name: "shield:stripe:review-code-node",
|
|
8538
|
+
tool: "bash",
|
|
8539
|
+
conditions: [
|
|
8540
|
+
{
|
|
8541
|
+
field: "command",
|
|
8542
|
+
op: "matches",
|
|
8543
|
+
// Node: stripe.refunds.create(...)
|
|
8544
|
+
value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
|
|
8545
|
+
},
|
|
8546
|
+
NOT_STRIPE_TEST
|
|
8547
|
+
],
|
|
8548
|
+
conditionMode: "all",
|
|
8549
|
+
verdict: "review",
|
|
8550
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8551
|
+
},
|
|
8552
|
+
{
|
|
8553
|
+
name: "shield:stripe:review-code-http",
|
|
8554
|
+
tool: "bash",
|
|
8555
|
+
conditions: [
|
|
8556
|
+
{
|
|
8557
|
+
field: "command",
|
|
8558
|
+
op: "matches",
|
|
8559
|
+
// requests.post("https://api.stripe.com/v1/refunds", ...)
|
|
8560
|
+
value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
|
|
8561
|
+
},
|
|
8562
|
+
NOT_STRIPE_TEST
|
|
8563
|
+
],
|
|
8564
|
+
conditionMode: "all",
|
|
8565
|
+
verdict: "review",
|
|
8566
|
+
reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
|
|
8567
|
+
}
|
|
8568
|
+
]
|
|
8569
|
+
},
|
|
8570
|
+
{
|
|
8571
|
+
name: "paypal",
|
|
8572
|
+
label: "PayPal",
|
|
8573
|
+
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.",
|
|
8574
|
+
template: "finance",
|
|
8575
|
+
server: "*paypal*",
|
|
8576
|
+
doors: [
|
|
8577
|
+
{
|
|
8578
|
+
name: "shield:paypal:review-api",
|
|
8579
|
+
tool: "bash",
|
|
8580
|
+
conditions: [
|
|
8581
|
+
{
|
|
8582
|
+
field: "command",
|
|
8583
|
+
op: "matches",
|
|
8584
|
+
// The live hosts only: api.paypal.com and api-m.paypal.com. The
|
|
8585
|
+
// sandbox (api-m.sandbox.paypal.com) moves no money.
|
|
8586
|
+
value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
|
|
8587
|
+
flags: "i"
|
|
8588
|
+
},
|
|
8589
|
+
{
|
|
8590
|
+
field: "command",
|
|
8591
|
+
op: "matches",
|
|
8592
|
+
// The endpoints that move money.
|
|
8593
|
+
value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
|
|
8594
|
+
flags: "i"
|
|
8595
|
+
},
|
|
8596
|
+
HTTP_WRITE
|
|
8597
|
+
],
|
|
8598
|
+
conditionMode: "all",
|
|
8599
|
+
verdict: "review",
|
|
8600
|
+
reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
|
|
8601
|
+
},
|
|
8602
|
+
{
|
|
8603
|
+
name: "shield:paypal:review-code",
|
|
8604
|
+
tool: "bash",
|
|
8605
|
+
conditions: [
|
|
8606
|
+
{
|
|
8607
|
+
field: "command",
|
|
8608
|
+
op: "matches",
|
|
8609
|
+
// The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
|
|
8610
|
+
// Python paypalrestsdk.
|
|
8611
|
+
value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
|
|
8612
|
+
}
|
|
8613
|
+
],
|
|
8614
|
+
verdict: "review",
|
|
8615
|
+
reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
|
|
8616
|
+
},
|
|
8617
|
+
{
|
|
8618
|
+
name: "shield:paypal:review-code-http",
|
|
8619
|
+
tool: "bash",
|
|
8620
|
+
conditions: [
|
|
8621
|
+
{
|
|
8622
|
+
field: "command",
|
|
8623
|
+
op: "matches",
|
|
8624
|
+
// The endpoints that move money; the token call every script
|
|
8625
|
+
// starts with (/v1/oauth2/token) is not one.
|
|
8626
|
+
value: postTo(
|
|
8627
|
+
"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)"
|
|
8628
|
+
)
|
|
8629
|
+
}
|
|
8630
|
+
],
|
|
8631
|
+
verdict: "review",
|
|
8632
|
+
reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
|
|
8633
|
+
}
|
|
8634
|
+
]
|
|
8635
|
+
},
|
|
8636
|
+
{
|
|
8637
|
+
name: "square",
|
|
8638
|
+
label: "Square",
|
|
8639
|
+
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.",
|
|
8640
|
+
template: "finance",
|
|
8641
|
+
// Square's servers, never Squarespace (a website builder).
|
|
8642
|
+
server: "{*square,*square[-_]*,*squareup*}",
|
|
8643
|
+
mcpTools: [
|
|
8644
|
+
{
|
|
8645
|
+
// Square's official MCP server has one tool for every API call; the
|
|
8646
|
+
// service and method name what it does.
|
|
8647
|
+
name: "shield:square:review-mcp-request",
|
|
8648
|
+
tool: "{,mcp__*__}make_api_request",
|
|
8649
|
+
conditions: [
|
|
8650
|
+
{
|
|
8651
|
+
field: "service",
|
|
8652
|
+
op: "matches",
|
|
8653
|
+
value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
|
|
8654
|
+
flags: "i"
|
|
8655
|
+
},
|
|
8656
|
+
{
|
|
8657
|
+
field: "method",
|
|
8658
|
+
op: "notMatches",
|
|
8659
|
+
value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
|
|
8660
|
+
flags: "i"
|
|
8661
|
+
}
|
|
8662
|
+
],
|
|
8663
|
+
conditionMode: "all",
|
|
8664
|
+
verdict: "review",
|
|
8665
|
+
reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
|
|
8666
|
+
}
|
|
8667
|
+
],
|
|
8668
|
+
doors: [
|
|
8669
|
+
{
|
|
8670
|
+
name: "shield:square:review-api",
|
|
8671
|
+
tool: "bash",
|
|
8672
|
+
conditions: [
|
|
8673
|
+
{
|
|
8674
|
+
field: "command",
|
|
8675
|
+
op: "matches",
|
|
8676
|
+
// The live host only; the sandbox is connect.squareupsandbox.com.
|
|
8677
|
+
value: httpTo(
|
|
8678
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8679
|
+
),
|
|
8680
|
+
flags: "i"
|
|
8681
|
+
},
|
|
8682
|
+
HTTP_WRITE_ANY
|
|
8683
|
+
],
|
|
8684
|
+
conditionMode: "all",
|
|
8685
|
+
verdict: "review",
|
|
8686
|
+
reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
|
|
8687
|
+
},
|
|
8688
|
+
{
|
|
8689
|
+
name: "shield:square:review-code",
|
|
8690
|
+
tool: "bash",
|
|
8691
|
+
conditions: [
|
|
8692
|
+
{
|
|
8693
|
+
field: "command",
|
|
8694
|
+
op: "matches",
|
|
8695
|
+
// The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
|
|
8696
|
+
value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
|
|
8697
|
+
},
|
|
8698
|
+
{
|
|
8699
|
+
field: "command",
|
|
8700
|
+
op: "matches",
|
|
8701
|
+
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*\\("
|
|
8702
|
+
}
|
|
8703
|
+
],
|
|
8704
|
+
conditionMode: "all",
|
|
8705
|
+
verdict: "review",
|
|
8706
|
+
reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
|
|
8707
|
+
},
|
|
8708
|
+
{
|
|
8709
|
+
name: "shield:square:review-code-http",
|
|
8710
|
+
tool: "bash",
|
|
8711
|
+
conditions: [
|
|
8712
|
+
{
|
|
8713
|
+
field: "command",
|
|
8714
|
+
op: "matches",
|
|
8715
|
+
value: postTo(
|
|
8716
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8717
|
+
)
|
|
8718
|
+
}
|
|
8719
|
+
],
|
|
8720
|
+
verdict: "review",
|
|
8721
|
+
reason: "This code posts to the Square API. node9 asks before money moves (Square)."
|
|
8722
|
+
}
|
|
8723
|
+
]
|
|
8724
|
+
},
|
|
8725
|
+
{
|
|
8726
|
+
name: "plaid",
|
|
8727
|
+
label: "Plaid",
|
|
8728
|
+
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.",
|
|
8729
|
+
template: "finance",
|
|
8730
|
+
server: "*plaid*",
|
|
8731
|
+
doors: [
|
|
8732
|
+
{
|
|
8733
|
+
name: "shield:plaid:review-api",
|
|
8734
|
+
tool: "bash",
|
|
8735
|
+
conditions: [
|
|
8736
|
+
{
|
|
8737
|
+
field: "command",
|
|
8738
|
+
op: "matches",
|
|
8739
|
+
// Every Plaid endpoint is a POST, so the path is what tells a
|
|
8740
|
+
// write; the live host only, never sandbox.plaid.com. A script
|
|
8741
|
+
// that posts to the URL meets this door too.
|
|
8742
|
+
value: httpTo(
|
|
8743
|
+
"production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
|
|
8744
|
+
),
|
|
8745
|
+
flags: "i"
|
|
8746
|
+
}
|
|
8747
|
+
],
|
|
8748
|
+
verdict: "review",
|
|
8749
|
+
reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
|
|
8750
|
+
},
|
|
8751
|
+
{
|
|
8752
|
+
name: "shield:plaid:review-code",
|
|
8753
|
+
tool: "bash",
|
|
8754
|
+
conditions: [
|
|
8755
|
+
{ field: "command", op: "matches", value: "\\bplaid", flags: "i" },
|
|
8756
|
+
{
|
|
8757
|
+
field: "command",
|
|
8758
|
+
op: "matches",
|
|
8759
|
+
value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
|
|
8760
|
+
}
|
|
8761
|
+
],
|
|
8762
|
+
conditionMode: "all",
|
|
8763
|
+
verdict: "review",
|
|
8764
|
+
reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
|
|
8765
|
+
}
|
|
8766
|
+
]
|
|
8767
|
+
},
|
|
8768
|
+
{
|
|
8769
|
+
name: "okta",
|
|
8770
|
+
label: "Okta",
|
|
8771
|
+
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.",
|
|
8772
|
+
template: "identity",
|
|
8773
|
+
server: "*okta*",
|
|
8774
|
+
doors: [
|
|
8775
|
+
{
|
|
8776
|
+
name: "shield:okta:review-api",
|
|
8777
|
+
tool: "bash",
|
|
8778
|
+
conditions: [
|
|
8779
|
+
{
|
|
8780
|
+
field: "command",
|
|
8781
|
+
op: "matches",
|
|
8782
|
+
// The management API only: sign-in (/api/v1/authn, /sessions,
|
|
8783
|
+
// /oauth2) is not under these paths.
|
|
8784
|
+
value: httpTo(
|
|
8785
|
+
"[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
|
|
8786
|
+
),
|
|
8787
|
+
flags: "i"
|
|
8788
|
+
},
|
|
8789
|
+
HTTP_WRITE_ANY
|
|
8790
|
+
],
|
|
8791
|
+
conditionMode: "all",
|
|
8792
|
+
verdict: "review",
|
|
8793
|
+
reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
|
|
8794
|
+
},
|
|
8795
|
+
{
|
|
8796
|
+
name: "shield:okta:review-code",
|
|
8797
|
+
tool: "bash",
|
|
8798
|
+
conditions: [
|
|
8799
|
+
{
|
|
8800
|
+
field: "command",
|
|
8801
|
+
op: "matches",
|
|
8802
|
+
// The Okta SDK in the script: a method named createUser alone is
|
|
8803
|
+
// in half the applications ever written.
|
|
8804
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8805
|
+
},
|
|
8806
|
+
{
|
|
8807
|
+
field: "command",
|
|
8808
|
+
op: "matches",
|
|
8809
|
+
value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
|
|
8810
|
+
}
|
|
8811
|
+
],
|
|
8812
|
+
conditionMode: "all",
|
|
8813
|
+
verdict: "review",
|
|
8814
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8815
|
+
},
|
|
8816
|
+
{
|
|
8817
|
+
name: "shield:okta:review-code-py",
|
|
8818
|
+
tool: "bash",
|
|
8819
|
+
conditions: [
|
|
8820
|
+
{
|
|
8821
|
+
field: "command",
|
|
8822
|
+
op: "matches",
|
|
8823
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8824
|
+
},
|
|
8825
|
+
{
|
|
8826
|
+
field: "command",
|
|
8827
|
+
op: "matches",
|
|
8828
|
+
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*\\("
|
|
8829
|
+
}
|
|
8830
|
+
],
|
|
8831
|
+
conditionMode: "all",
|
|
8832
|
+
verdict: "review",
|
|
8833
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8834
|
+
}
|
|
8835
|
+
]
|
|
8836
|
+
},
|
|
8837
|
+
{
|
|
8838
|
+
name: "auth0",
|
|
8839
|
+
label: "Auth0",
|
|
8840
|
+
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.",
|
|
8841
|
+
template: "identity",
|
|
8842
|
+
server: "*auth0*",
|
|
8843
|
+
mcpTools: [
|
|
8844
|
+
{
|
|
8845
|
+
// Auth0's official MCP server: applications (their secrets and
|
|
8846
|
+
// callbacks), APIs and their scopes, and Actions (code that runs at
|
|
8847
|
+
// every sign-in).
|
|
8848
|
+
name: "shield:auth0:review-tenant-config",
|
|
8849
|
+
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}",
|
|
8850
|
+
conditions: [],
|
|
8851
|
+
verdict: "review",
|
|
8852
|
+
reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
|
|
8853
|
+
}
|
|
8854
|
+
],
|
|
8855
|
+
doors: [
|
|
8856
|
+
{
|
|
8857
|
+
name: "shield:auth0:review-cli",
|
|
8858
|
+
tool: "bash",
|
|
8859
|
+
conditions: [
|
|
8860
|
+
{
|
|
8861
|
+
field: "command",
|
|
8862
|
+
op: "matches",
|
|
8863
|
+
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-])",
|
|
8864
|
+
flags: "i"
|
|
8865
|
+
}
|
|
8866
|
+
],
|
|
8867
|
+
verdict: "review",
|
|
8868
|
+
reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8869
|
+
},
|
|
8870
|
+
{
|
|
8871
|
+
name: "shield:auth0:review-api",
|
|
8872
|
+
tool: "bash",
|
|
8873
|
+
conditions: [
|
|
8874
|
+
{
|
|
8875
|
+
field: "command",
|
|
8876
|
+
op: "matches",
|
|
8877
|
+
// The Management API only; /oauth/token is signing in.
|
|
8878
|
+
value: httpTo(
|
|
8879
|
+
"\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
|
|
8880
|
+
),
|
|
8881
|
+
flags: "i"
|
|
8882
|
+
},
|
|
8883
|
+
HTTP_WRITE_ANY
|
|
8884
|
+
],
|
|
8885
|
+
conditionMode: "all",
|
|
8886
|
+
verdict: "review",
|
|
8887
|
+
reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8888
|
+
},
|
|
8889
|
+
{
|
|
8890
|
+
name: "shield:auth0:review-code",
|
|
8891
|
+
tool: "bash",
|
|
8892
|
+
conditions: [
|
|
8893
|
+
{
|
|
8894
|
+
field: "command",
|
|
8895
|
+
op: "matches",
|
|
8896
|
+
// The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
|
|
8897
|
+
value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
|
|
8898
|
+
},
|
|
8899
|
+
{
|
|
8900
|
+
field: "command",
|
|
8901
|
+
op: "matches",
|
|
8902
|
+
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*\\("
|
|
8903
|
+
}
|
|
8904
|
+
],
|
|
8905
|
+
conditionMode: "all",
|
|
8906
|
+
verdict: "review",
|
|
8907
|
+
reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
|
|
8908
|
+
}
|
|
8909
|
+
]
|
|
8910
|
+
}
|
|
8911
|
+
];
|
|
8912
|
+
APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
|
|
8913
|
+
APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
|
|
8914
|
+
APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
|
|
8915
|
+
GENERAL_ACTION_RULES = buildGeneralRules();
|
|
8115
8916
|
BUILTIN_SHIELDS = {
|
|
8116
8917
|
[aws_default.name]: aws_default,
|
|
8117
8918
|
[bash_safe_default.name]: bash_safe_default,
|
|
8118
8919
|
[docker_default.name]: docker_default,
|
|
8119
8920
|
[filesystem_default.name]: filesystem_default,
|
|
8120
|
-
// Dangerous actions as category shields (node9Firewall#385): an MCP app's
|
|
8121
|
-
// money, credential and publishing tools, asked about whatever the request
|
|
8122
|
-
// said. One glob per rule covers the bare name the gateway sends and the
|
|
8123
|
-
// mcp__<server>__ name the agent hook sends.
|
|
8124
|
-
[finance_default.name]: finance_default,
|
|
8125
8921
|
[github_default.name]: github_default,
|
|
8126
|
-
[identity_default.name]: identity_default,
|
|
8127
8922
|
[k8s_default.name]: k8s_default,
|
|
8128
8923
|
[mongodb_default.name]: mongodb_default,
|
|
8129
8924
|
[postgres_default.name]: postgres_default,
|
|
8130
8925
|
[project_jail_default.name]: project_jail_default,
|
|
8131
|
-
[
|
|
8132
|
-
|
|
8926
|
+
[redis_default.name]: redis_default,
|
|
8927
|
+
// App shields generated from the action templates (node9Firewall#390): one
|
|
8928
|
+
// per app, every door (MCP server, CLI, API, library), off by default.
|
|
8929
|
+
...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
|
|
8133
8930
|
};
|
|
8134
8931
|
assertBuiltinShieldRegexesAreSafe();
|
|
8135
8932
|
SSRF_MAX_HOST = 253;
|
|
@@ -8185,9 +8982,9 @@ var init_dist = __esm({
|
|
|
8185
8982
|
},
|
|
8186
8983
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
8187
8984
|
"commands.inline-exec": {
|
|
8188
|
-
plain: "The agent runs a small program written inside the command itself.",
|
|
8985
|
+
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.",
|
|
8189
8986
|
example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
|
|
8190
|
-
advice: "
|
|
8987
|
+
advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
|
|
8191
8988
|
},
|
|
8192
8989
|
"commands.eval-dynamic": {
|
|
8193
8990
|
plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
|
|
@@ -8219,6 +9016,21 @@ var init_dist = __esm({
|
|
|
8219
9016
|
example: "chmod 777 deploy.sh",
|
|
8220
9017
|
advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
|
|
8221
9018
|
},
|
|
9019
|
+
"actions.money": {
|
|
9020
|
+
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.",
|
|
9021
|
+
example: "refund_order on a server called acme-billing",
|
|
9022
|
+
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."
|
|
9023
|
+
},
|
|
9024
|
+
"actions.access": {
|
|
9025
|
+
plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
|
|
9026
|
+
example: "invite_user or grant_role on a company admin server",
|
|
9027
|
+
advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
|
|
9028
|
+
},
|
|
9029
|
+
"actions.publish": {
|
|
9030
|
+
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.",
|
|
9031
|
+
example: "create_release or deploy_to_production on a company MCP server",
|
|
9032
|
+
advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
|
|
9033
|
+
},
|
|
8222
9034
|
"commands.sudo": {
|
|
8223
9035
|
plain: "The agent runs a command as the administrator (root).",
|
|
8224
9036
|
example: "sudo apt-get install -y nginx",
|
|
@@ -8379,7 +9191,7 @@ var init_dist = __esm({
|
|
|
8379
9191
|
advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
|
|
8380
9192
|
},
|
|
8381
9193
|
"loading.malicious-package": {
|
|
8382
|
-
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.",
|
|
9194
|
+
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.",
|
|
8383
9195
|
example: "npm install of a package flagged as malware",
|
|
8384
9196
|
advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
|
|
8385
9197
|
}
|
|
@@ -8431,7 +9243,7 @@ var init_dist = __esm({
|
|
|
8431
9243
|
id: "commands.inline-exec",
|
|
8432
9244
|
group: "commands",
|
|
8433
9245
|
title: "Inline code execution",
|
|
8434
|
-
catches: "
|
|
9246
|
+
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",
|
|
8435
9247
|
values: ALL,
|
|
8436
9248
|
defaultValue: "review",
|
|
8437
9249
|
legacy: ["Node9 Standard (Inline Execution)"]
|
|
@@ -8788,6 +9600,38 @@ var init_dist = __esm({
|
|
|
8788
9600
|
values: ALL,
|
|
8789
9601
|
defaultValue: "block",
|
|
8790
9602
|
legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
|
|
9603
|
+
},
|
|
9604
|
+
// ── Actions in apps (node9Firewall#390) ──────────────────────────────────
|
|
9605
|
+
// The safety net under the app shields: an action that moves money, changes
|
|
9606
|
+
// access or publishes, on an MCP server no app shield claims (an internal
|
|
9607
|
+
// billing server, a vendor with no shield, Zapier). Review by default; the
|
|
9608
|
+
// rules come from the action templates (shields/apps.ts).
|
|
9609
|
+
{
|
|
9610
|
+
id: "actions.money",
|
|
9611
|
+
group: "actions",
|
|
9612
|
+
title: "Money moves",
|
|
9613
|
+
catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
|
|
9614
|
+
values: ALL,
|
|
9615
|
+
defaultValue: "review",
|
|
9616
|
+
legacy: generalActionRuleNames("finance")
|
|
9617
|
+
},
|
|
9618
|
+
{
|
|
9619
|
+
id: "actions.access",
|
|
9620
|
+
group: "actions",
|
|
9621
|
+
title: "Accounts and access change",
|
|
9622
|
+
catches: "a password, key, role, permission or user changed through an app with no protection of its own",
|
|
9623
|
+
values: ALL,
|
|
9624
|
+
defaultValue: "review",
|
|
9625
|
+
legacy: generalActionRuleNames("identity")
|
|
9626
|
+
},
|
|
9627
|
+
{
|
|
9628
|
+
id: "actions.publish",
|
|
9629
|
+
group: "actions",
|
|
9630
|
+
title: "Something is published",
|
|
9631
|
+
catches: "a release, package, deploy or public post through an app with no protection of its own",
|
|
9632
|
+
values: ALL,
|
|
9633
|
+
defaultValue: "review",
|
|
9634
|
+
legacy: generalActionRuleNames("publishing")
|
|
8791
9635
|
}
|
|
8792
9636
|
];
|
|
8793
9637
|
SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
|
|
@@ -8910,12 +9754,223 @@ var init_dist = __esm({
|
|
|
8910
9754
|
\u03A5: "Y",
|
|
8911
9755
|
\u03A7: "X"
|
|
8912
9756
|
};
|
|
8913
|
-
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
8914
|
-
MAX_BLOBS = 32;
|
|
8915
|
-
MIN_DECODED = 16;
|
|
8916
|
-
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
8917
|
-
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
8918
|
-
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
9757
|
+
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
9758
|
+
MAX_BLOBS = 32;
|
|
9759
|
+
MIN_DECODED = 16;
|
|
9760
|
+
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
9761
|
+
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
9762
|
+
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
9763
|
+
SYSTEM_AUTH_FILES = [
|
|
9764
|
+
"/etc/passwd",
|
|
9765
|
+
"/etc/shadow",
|
|
9766
|
+
"/etc/group",
|
|
9767
|
+
"/etc/gshadow",
|
|
9768
|
+
"/etc/sudoers"
|
|
9769
|
+
];
|
|
9770
|
+
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9771
|
+
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9772
|
+
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9773
|
+
SYSTEM_DIRS = [
|
|
9774
|
+
"/usr",
|
|
9775
|
+
"/etc",
|
|
9776
|
+
"/boot",
|
|
9777
|
+
"/bin",
|
|
9778
|
+
"/sbin",
|
|
9779
|
+
"/lib",
|
|
9780
|
+
"/lib64",
|
|
9781
|
+
"/var/log"
|
|
9782
|
+
];
|
|
9783
|
+
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
9784
|
+
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
9785
|
+
AGENT_INSTRUCTION_FILES = [
|
|
9786
|
+
".gitlab-ci.yml",
|
|
9787
|
+
"CLAUDE.md",
|
|
9788
|
+
"AGENTS.md",
|
|
9789
|
+
".cursorrules",
|
|
9790
|
+
".mcp.json",
|
|
9791
|
+
".vscode/settings.json",
|
|
9792
|
+
"package.json"
|
|
9793
|
+
];
|
|
9794
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
9795
|
+
".github/workflows",
|
|
9796
|
+
".claude",
|
|
9797
|
+
".cursor",
|
|
9798
|
+
".git/hooks",
|
|
9799
|
+
".husky"
|
|
9800
|
+
];
|
|
9801
|
+
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
9802
|
+
MAX_INLINE_CODE = 64 * 1024;
|
|
9803
|
+
WHAT = {
|
|
9804
|
+
deletes: "deletes files",
|
|
9805
|
+
"writes-outside": "writes a file outside the project",
|
|
9806
|
+
sends: "sends data over the network",
|
|
9807
|
+
listens: "opens a listening socket or a shell",
|
|
9808
|
+
"runs-commands": "runs other commands or processes",
|
|
9809
|
+
"reads-protected": "reads a protected file",
|
|
9810
|
+
"builds-code": "runs code it builds at run time",
|
|
9811
|
+
permissions: "changes permissions or ownership",
|
|
9812
|
+
monitors: "captures the screen, the clipboard or the keyboard",
|
|
9813
|
+
amplifies: "writes or sends in an endless loop"
|
|
9814
|
+
};
|
|
9815
|
+
OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
|
|
9816
|
+
pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
|
|
9817
|
+
either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
|
|
9818
|
+
PYTHON = [
|
|
9819
|
+
[
|
|
9820
|
+
"deletes",
|
|
9821
|
+
/\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/
|
|
9822
|
+
],
|
|
9823
|
+
[
|
|
9824
|
+
"writes-outside",
|
|
9825
|
+
new RegExp(
|
|
9826
|
+
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}`
|
|
9827
|
+
)
|
|
9828
|
+
],
|
|
9829
|
+
[
|
|
9830
|
+
"sends",
|
|
9831
|
+
either(
|
|
9832
|
+
pyImport(
|
|
9833
|
+
String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
|
|
9834
|
+
),
|
|
9835
|
+
/\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
|
|
9836
|
+
)
|
|
9837
|
+
],
|
|
9838
|
+
[
|
|
9839
|
+
"listens",
|
|
9840
|
+
either(
|
|
9841
|
+
pyImport(String.raw`pty|socketserver|http\.server`),
|
|
9842
|
+
/\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
|
|
9843
|
+
)
|
|
9844
|
+
],
|
|
9845
|
+
[
|
|
9846
|
+
"runs-commands",
|
|
9847
|
+
either(
|
|
9848
|
+
pyImport("subprocess|pexpect|commands|psutil"),
|
|
9849
|
+
/\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+\*/
|
|
9850
|
+
)
|
|
9851
|
+
],
|
|
9852
|
+
[
|
|
9853
|
+
"builds-code",
|
|
9854
|
+
/(?<![\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*\[/
|
|
9855
|
+
],
|
|
9856
|
+
[
|
|
9857
|
+
// Spyware shapes: a screenshot, the clipboard, a key logger.
|
|
9858
|
+
"monitors",
|
|
9859
|
+
either(
|
|
9860
|
+
pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
|
|
9861
|
+
/\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
|
|
9862
|
+
)
|
|
9863
|
+
],
|
|
9864
|
+
[
|
|
9865
|
+
"permissions",
|
|
9866
|
+
/\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
|
|
9867
|
+
]
|
|
9868
|
+
];
|
|
9869
|
+
NODE = [
|
|
9870
|
+
[
|
|
9871
|
+
"deletes",
|
|
9872
|
+
/\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
|
|
9873
|
+
],
|
|
9874
|
+
[
|
|
9875
|
+
"writes-outside",
|
|
9876
|
+
new RegExp(
|
|
9877
|
+
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}`
|
|
9878
|
+
)
|
|
9879
|
+
],
|
|
9880
|
+
[
|
|
9881
|
+
"sends",
|
|
9882
|
+
/\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*\(/
|
|
9883
|
+
],
|
|
9884
|
+
["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
|
|
9885
|
+
[
|
|
9886
|
+
"runs-commands",
|
|
9887
|
+
/\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
|
|
9888
|
+
],
|
|
9889
|
+
[
|
|
9890
|
+
"builds-code",
|
|
9891
|
+
/(?<![\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)]/
|
|
9892
|
+
],
|
|
9893
|
+
[
|
|
9894
|
+
"permissions",
|
|
9895
|
+
/\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
|
|
9896
|
+
]
|
|
9897
|
+
];
|
|
9898
|
+
PERL = [
|
|
9899
|
+
["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
|
|
9900
|
+
[
|
|
9901
|
+
"writes-outside",
|
|
9902
|
+
new RegExp(
|
|
9903
|
+
String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
|
|
9904
|
+
)
|
|
9905
|
+
],
|
|
9906
|
+
[
|
|
9907
|
+
"sends",
|
|
9908
|
+
/\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
|
|
9909
|
+
],
|
|
9910
|
+
[
|
|
9911
|
+
"runs-commands",
|
|
9912
|
+
/\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
|
|
9913
|
+
],
|
|
9914
|
+
["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
|
|
9915
|
+
["permissions", /\b(?:chmod|chown)\b/]
|
|
9916
|
+
];
|
|
9917
|
+
RUBY = [
|
|
9918
|
+
[
|
|
9919
|
+
"deletes",
|
|
9920
|
+
/\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
|
|
9921
|
+
],
|
|
9922
|
+
[
|
|
9923
|
+
"writes-outside",
|
|
9924
|
+
new RegExp(
|
|
9925
|
+
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}`
|
|
9926
|
+
)
|
|
9927
|
+
],
|
|
9928
|
+
[
|
|
9929
|
+
"sends",
|
|
9930
|
+
/\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)['"]/
|
|
9931
|
+
],
|
|
9932
|
+
["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
|
|
9933
|
+
[
|
|
9934
|
+
"runs-commands",
|
|
9935
|
+
/(?<![\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/
|
|
9936
|
+
],
|
|
9937
|
+
[
|
|
9938
|
+
"builds-code",
|
|
9939
|
+
/(?<![\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/
|
|
9940
|
+
],
|
|
9941
|
+
["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
|
|
9942
|
+
];
|
|
9943
|
+
PHP = [
|
|
9944
|
+
["deletes", /\b(?:unlink|rmdir)\s*\(/],
|
|
9945
|
+
[
|
|
9946
|
+
"writes-outside",
|
|
9947
|
+
new RegExp(
|
|
9948
|
+
String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
|
|
9949
|
+
)
|
|
9950
|
+
],
|
|
9951
|
+
[
|
|
9952
|
+
"sends",
|
|
9953
|
+
/\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):/
|
|
9954
|
+
],
|
|
9955
|
+
["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
|
|
9956
|
+
[
|
|
9957
|
+
"runs-commands",
|
|
9958
|
+
/\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
|
|
9959
|
+
],
|
|
9960
|
+
[
|
|
9961
|
+
"builds-code",
|
|
9962
|
+
/\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
|
|
9963
|
+
],
|
|
9964
|
+
["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
|
|
9965
|
+
];
|
|
9966
|
+
TABLES = {
|
|
9967
|
+
python: PYTHON,
|
|
9968
|
+
node: NODE,
|
|
9969
|
+
perl: PERL,
|
|
9970
|
+
ruby: RUBY,
|
|
9971
|
+
php: PHP
|
|
9972
|
+
};
|
|
9973
|
+
STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
|
|
8919
9974
|
({ syntax } = import_mvdan_sh.default);
|
|
8920
9975
|
sharedParser = syntax.NewParser();
|
|
8921
9976
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -8953,6 +10008,10 @@ var init_dist = __esm({
|
|
|
8953
10008
|
AST_CACHE_MAX = 5e3;
|
|
8954
10009
|
astCache = /* @__PURE__ */ new Map();
|
|
8955
10010
|
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
10011
|
+
INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
|
|
10012
|
+
PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
|
|
10013
|
+
ACCESS_VIEW_CACHE_MAX = 1e3;
|
|
10014
|
+
accessViewCache = /* @__PURE__ */ new Map();
|
|
8956
10015
|
MAX_READING_PAYLOADS = 8;
|
|
8957
10016
|
defaultExpansionOps = null;
|
|
8958
10017
|
VARIABLE_READERS = /* @__PURE__ */ new Set([
|
|
@@ -9662,6 +10721,14 @@ var init_dist = __esm({
|
|
|
9662
10721
|
INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
9663
10722
|
MAX_PAYLOAD_NESTING = 8;
|
|
9664
10723
|
payloadNesting = 0;
|
|
10724
|
+
MAX_INLINE_CALLS = 16;
|
|
10725
|
+
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;
|
|
10726
|
+
UNREADABLE_SOURCE = {
|
|
10727
|
+
pipe: "reads its code from another program through a pipe",
|
|
10728
|
+
file: "reads its code from a file on stdin",
|
|
10729
|
+
variable: "runs code the shell fills in at run time",
|
|
10730
|
+
payload: "runs a shell payload that evaluates, sources or runs further code"
|
|
10731
|
+
};
|
|
9665
10732
|
SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
|
|
9666
10733
|
SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
|
|
9667
10734
|
MAX_SCRIPT_OPERANDS = 8;
|
|
@@ -9861,9 +10928,6 @@ var init_dist = __esm({
|
|
|
9861
10928
|
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
9862
10929
|
NONE = { kind: "none" };
|
|
9863
10930
|
UNKNOWN = { kind: "unknown" };
|
|
9864
|
-
MAX_REGEX_LENGTH = 256;
|
|
9865
|
-
REGEX_CACHE_MAX = 500;
|
|
9866
|
-
regexCache = /* @__PURE__ */ new Map();
|
|
9867
10931
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
9868
10932
|
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9869
10933
|
"bash",
|
|
@@ -9978,45 +11042,6 @@ var init_dist = __esm({
|
|
|
9978
11042
|
(s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
|
|
9979
11043
|
)
|
|
9980
11044
|
);
|
|
9981
|
-
SYSTEM_AUTH_FILES = [
|
|
9982
|
-
"/etc/passwd",
|
|
9983
|
-
"/etc/shadow",
|
|
9984
|
-
"/etc/group",
|
|
9985
|
-
"/etc/gshadow",
|
|
9986
|
-
"/etc/sudoers"
|
|
9987
|
-
];
|
|
9988
|
-
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9989
|
-
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9990
|
-
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9991
|
-
SYSTEM_DIRS = [
|
|
9992
|
-
"/usr",
|
|
9993
|
-
"/etc",
|
|
9994
|
-
"/boot",
|
|
9995
|
-
"/bin",
|
|
9996
|
-
"/sbin",
|
|
9997
|
-
"/lib",
|
|
9998
|
-
"/lib64",
|
|
9999
|
-
"/var/log"
|
|
10000
|
-
];
|
|
10001
|
-
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
10002
|
-
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
10003
|
-
AGENT_INSTRUCTION_FILES = [
|
|
10004
|
-
".gitlab-ci.yml",
|
|
10005
|
-
"CLAUDE.md",
|
|
10006
|
-
"AGENTS.md",
|
|
10007
|
-
".cursorrules",
|
|
10008
|
-
".mcp.json",
|
|
10009
|
-
".vscode/settings.json",
|
|
10010
|
-
"package.json"
|
|
10011
|
-
];
|
|
10012
|
-
AGENT_INSTRUCTION_DIRS = [
|
|
10013
|
-
".github/workflows",
|
|
10014
|
-
".claude",
|
|
10015
|
-
".cursor",
|
|
10016
|
-
".git/hooks",
|
|
10017
|
-
".husky"
|
|
10018
|
-
];
|
|
10019
|
-
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
10020
11045
|
exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
|
|
10021
11046
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
10022
11047
|
BIP39_ENGLISH = `abandon
|
|
@@ -13509,7 +14534,11 @@ zoo`.split("\n");
|
|
|
13509
14534
|
"commands.disk-destroy",
|
|
13510
14535
|
"commands.dangerous-word",
|
|
13511
14536
|
"commands.node9-disarm",
|
|
13512
|
-
"data.pipe-chain"
|
|
14537
|
+
"data.pipe-chain",
|
|
14538
|
+
// The general action checks (node9Firewall#390): smart rules the map governs.
|
|
14539
|
+
"actions.money",
|
|
14540
|
+
"actions.access",
|
|
14541
|
+
"actions.publish"
|
|
13513
14542
|
]);
|
|
13514
14543
|
KNOB_GOVERNED = {
|
|
13515
14544
|
"data.secrets": ["off", "block"],
|
|
@@ -14224,7 +15253,8 @@ var SmartConditionSchema = import_zod.z.object({
|
|
|
14224
15253
|
}
|
|
14225
15254
|
),
|
|
14226
15255
|
value: import_zod.z.string().optional(),
|
|
14227
|
-
flags: import_zod.z.string().optional()
|
|
15256
|
+
flags: import_zod.z.string().optional(),
|
|
15257
|
+
reading: import_zod.z.enum(["access"]).optional()
|
|
14228
15258
|
}).refine(
|
|
14229
15259
|
(c) => {
|
|
14230
15260
|
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
@@ -14235,6 +15265,9 @@ var SmartConditionSchema = import_zod.z.object({
|
|
|
14235
15265
|
var SmartRuleSchema = import_zod.z.object({
|
|
14236
15266
|
name: import_zod.z.string().optional(),
|
|
14237
15267
|
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
15268
|
+
// Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
|
|
15269
|
+
server: import_zod.z.string().min(1).optional(),
|
|
15270
|
+
notServer: import_zod.z.string().min(1).optional(),
|
|
14238
15271
|
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
14239
15272
|
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
14240
15273
|
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
@@ -14384,8 +15417,8 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
14384
15417
|
}).strict();
|
|
14385
15418
|
function formatIssues(issues) {
|
|
14386
15419
|
const lines = issues.map((issue) => {
|
|
14387
|
-
const
|
|
14388
|
-
return ` \u2022 ${
|
|
15420
|
+
const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
15421
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
14389
15422
|
});
|
|
14390
15423
|
return `Invalid config:
|
|
14391
15424
|
${lines.join("\n")}`;
|
|
@@ -14398,9 +15431,9 @@ function prunePaths(root, paths) {
|
|
|
14398
15431
|
const yi = y[y.length - 1];
|
|
14399
15432
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
14400
15433
|
});
|
|
14401
|
-
for (const
|
|
15434
|
+
for (const path28 of ordered) {
|
|
14402
15435
|
let cur = root;
|
|
14403
|
-
for (const key of
|
|
15436
|
+
for (const key of path28.slice(0, -1)) {
|
|
14404
15437
|
if (cur === null || typeof cur !== "object") {
|
|
14405
15438
|
cur = void 0;
|
|
14406
15439
|
break;
|
|
@@ -14408,7 +15441,7 @@ function prunePaths(root, paths) {
|
|
|
14408
15441
|
cur = cur[key];
|
|
14409
15442
|
}
|
|
14410
15443
|
if (cur === null || typeof cur !== "object") continue;
|
|
14411
|
-
const last =
|
|
15444
|
+
const last = path28[path28.length - 1];
|
|
14412
15445
|
if (Array.isArray(cur)) {
|
|
14413
15446
|
const i = Number(last);
|
|
14414
15447
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -14444,7 +15477,7 @@ function sanitizeConfig(raw) {
|
|
|
14444
15477
|
return issue.keys.map((k) => [...at, k]);
|
|
14445
15478
|
}
|
|
14446
15479
|
return [at.slice(0, -level || void 0)];
|
|
14447
|
-
}).filter((
|
|
15480
|
+
}).filter((path28) => path28.length > 0);
|
|
14448
15481
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
14449
15482
|
}
|
|
14450
15483
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -14470,6 +15503,85 @@ var import_fs2 = __toESM(require("fs"));
|
|
|
14470
15503
|
var import_path2 = __toESM(require("path"));
|
|
14471
15504
|
var import_os2 = __toESM(require("os"));
|
|
14472
15505
|
init_dist();
|
|
15506
|
+
|
|
15507
|
+
// src/shields/build.ts
|
|
15508
|
+
init_dist();
|
|
15509
|
+
function escapeRegex(s) {
|
|
15510
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15511
|
+
}
|
|
15512
|
+
function slug(s) {
|
|
15513
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
15514
|
+
}
|
|
15515
|
+
var B = "[\\s/\\\\]";
|
|
15516
|
+
var SEP = "[/\\\\]";
|
|
15517
|
+
function pathToRegexFragment(rawPath) {
|
|
15518
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
15519
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
15520
|
+
if (segments2.length === 0) return "";
|
|
15521
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
15522
|
+
}
|
|
15523
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
15524
|
+
const value = pathToRegexFragment(rawPath);
|
|
15525
|
+
if (!value || !candidate) return false;
|
|
15526
|
+
const re = getCompiledRegex(value);
|
|
15527
|
+
if (!re) return false;
|
|
15528
|
+
return re.test(candidate);
|
|
15529
|
+
}
|
|
15530
|
+
function pathRules(rawPath, verdict, reason) {
|
|
15531
|
+
const value = pathToRegexFragment(rawPath);
|
|
15532
|
+
if (!value) return [];
|
|
15533
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
15534
|
+
const s = slug(rawPath);
|
|
15535
|
+
return [
|
|
15536
|
+
// `reading: 'access'`: a script's interpreter line and the program a
|
|
15537
|
+
// command runs are not reads of the jailed path (node9Firewall#385).
|
|
15538
|
+
{
|
|
15539
|
+
name: `${verdict}-path-${s}-bash`,
|
|
15540
|
+
tool: "bash",
|
|
15541
|
+
conditions: [{ field: "command", op: "matches", value, reading: "access" }],
|
|
15542
|
+
verdict,
|
|
15543
|
+
reason: why
|
|
15544
|
+
},
|
|
15545
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
15546
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
15547
|
+
{
|
|
15548
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
15549
|
+
tool: "*",
|
|
15550
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
15551
|
+
verdict,
|
|
15552
|
+
reason: why
|
|
15553
|
+
},
|
|
15554
|
+
{
|
|
15555
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
15556
|
+
tool: "*",
|
|
15557
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
15558
|
+
verdict,
|
|
15559
|
+
reason: why
|
|
15560
|
+
},
|
|
15561
|
+
{
|
|
15562
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
15563
|
+
tool: "*",
|
|
15564
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
15565
|
+
verdict,
|
|
15566
|
+
reason: why
|
|
15567
|
+
}
|
|
15568
|
+
];
|
|
15569
|
+
}
|
|
15570
|
+
var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
|
|
15571
|
+
function withAccessReading(def) {
|
|
15572
|
+
let changed = false;
|
|
15573
|
+
const smartRules = def.smartRules.map((r) => {
|
|
15574
|
+
const [c, ...rest] = r.conditions ?? [];
|
|
15575
|
+
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) {
|
|
15576
|
+
return r;
|
|
15577
|
+
}
|
|
15578
|
+
changed = true;
|
|
15579
|
+
return { ...r, conditions: [{ ...c, reading: "access" }] };
|
|
15580
|
+
});
|
|
15581
|
+
return changed ? { ...def, smartRules } : def;
|
|
15582
|
+
}
|
|
15583
|
+
|
|
15584
|
+
// src/shields.ts
|
|
14473
15585
|
init_dist();
|
|
14474
15586
|
var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
|
|
14475
15587
|
function loadUserShields() {
|
|
@@ -14496,7 +15608,7 @@ function loadUserShields() {
|
|
|
14496
15608
|
`);
|
|
14497
15609
|
continue;
|
|
14498
15610
|
}
|
|
14499
|
-
result[v.ok.name] = v.ok;
|
|
15611
|
+
result[v.ok.name] = withAccessReading(v.ok);
|
|
14500
15612
|
} catch (err) {
|
|
14501
15613
|
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
14502
15614
|
`);
|
|
@@ -14648,90 +15760,28 @@ var CLASS_B_COMMAND_CHECKS = /* @__PURE__ */ new Set([
|
|
|
14648
15760
|
"evalDynamic",
|
|
14649
15761
|
"pipeChainHigh"
|
|
14650
15762
|
]);
|
|
14651
|
-
function applyManagedCommandChecks(local, managed, locked) {
|
|
14652
|
-
const next = { ...local };
|
|
14653
|
-
for (const key of COMMAND_CHECK_KEYS) {
|
|
14654
|
-
const m = managed[key];
|
|
14655
|
-
if (typeof m !== "string") continue;
|
|
14656
|
-
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
14657
|
-
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
14658
|
-
locked: locked.includes(lockKey)
|
|
14659
|
-
});
|
|
14660
|
-
if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
|
|
14661
|
-
next[key] = resolved;
|
|
14662
|
-
}
|
|
14663
|
-
return next;
|
|
14664
|
-
}
|
|
14665
|
-
function applyManagedApprovers(local, managed) {
|
|
14666
|
-
return {
|
|
14667
|
-
...local,
|
|
14668
|
-
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
14669
|
-
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
14670
|
-
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
14671
|
-
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
14672
|
-
};
|
|
14673
|
-
}
|
|
14674
|
-
|
|
14675
|
-
// src/shields/build.ts
|
|
14676
|
-
init_dist();
|
|
14677
|
-
function escapeRegex(s) {
|
|
14678
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14679
|
-
}
|
|
14680
|
-
function slug(s) {
|
|
14681
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
14682
|
-
}
|
|
14683
|
-
var B = "[\\s/\\\\]";
|
|
14684
|
-
var SEP = "[/\\\\]";
|
|
14685
|
-
function pathToRegexFragment(rawPath) {
|
|
14686
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
14687
|
-
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
14688
|
-
if (segments2.length === 0) return "";
|
|
14689
|
-
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
14690
|
-
}
|
|
14691
|
-
function pathMatchesFragment(candidate, rawPath) {
|
|
14692
|
-
const value = pathToRegexFragment(rawPath);
|
|
14693
|
-
if (!value || !candidate) return false;
|
|
14694
|
-
const re = getCompiledRegex(value);
|
|
14695
|
-
if (!re) return false;
|
|
14696
|
-
return re.test(candidate);
|
|
14697
|
-
}
|
|
14698
|
-
function pathRules(rawPath, verdict, reason) {
|
|
14699
|
-
const value = pathToRegexFragment(rawPath);
|
|
14700
|
-
if (!value) return [];
|
|
14701
|
-
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
14702
|
-
const s = slug(rawPath);
|
|
14703
|
-
return [
|
|
14704
|
-
{
|
|
14705
|
-
name: `${verdict}-path-${s}-bash`,
|
|
14706
|
-
tool: "bash",
|
|
14707
|
-
conditions: [{ field: "command", op: "matches", value }],
|
|
14708
|
-
verdict,
|
|
14709
|
-
reason: why
|
|
14710
|
-
},
|
|
14711
|
-
// Keep the historical `-anytool` name for the file_path rule: the
|
|
14712
|
-
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
14713
|
-
{
|
|
14714
|
-
name: `${verdict}-path-${s}-anytool`,
|
|
14715
|
-
tool: "*",
|
|
14716
|
-
conditions: [{ field: "file_path", op: "matches", value }],
|
|
14717
|
-
verdict,
|
|
14718
|
-
reason: why
|
|
14719
|
-
},
|
|
14720
|
-
{
|
|
14721
|
-
name: `${verdict}-path-${s}-anytool-path`,
|
|
14722
|
-
tool: "*",
|
|
14723
|
-
conditions: [{ field: "path", op: "matches", value }],
|
|
14724
|
-
verdict,
|
|
14725
|
-
reason: why
|
|
14726
|
-
},
|
|
14727
|
-
{
|
|
14728
|
-
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
14729
|
-
tool: "*",
|
|
14730
|
-
conditions: [{ field: "pattern", op: "matches", value }],
|
|
14731
|
-
verdict,
|
|
14732
|
-
reason: why
|
|
14733
|
-
}
|
|
14734
|
-
];
|
|
15763
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
15764
|
+
const next = { ...local };
|
|
15765
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
15766
|
+
const m = managed[key];
|
|
15767
|
+
if (typeof m !== "string") continue;
|
|
15768
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
15769
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
15770
|
+
locked: locked.includes(lockKey)
|
|
15771
|
+
});
|
|
15772
|
+
if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
|
|
15773
|
+
next[key] = resolved;
|
|
15774
|
+
}
|
|
15775
|
+
return next;
|
|
15776
|
+
}
|
|
15777
|
+
function applyManagedApprovers(local, managed) {
|
|
15778
|
+
return {
|
|
15779
|
+
...local,
|
|
15780
|
+
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
15781
|
+
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
15782
|
+
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
15783
|
+
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
15784
|
+
};
|
|
14735
15785
|
}
|
|
14736
15786
|
|
|
14737
15787
|
// src/auth/trusted-hosts.ts
|
|
@@ -14792,6 +15842,7 @@ function isTrustedHost(host2) {
|
|
|
14792
15842
|
|
|
14793
15843
|
// src/config/index.ts
|
|
14794
15844
|
init_dist();
|
|
15845
|
+
init_dist();
|
|
14795
15846
|
|
|
14796
15847
|
// src/config/v2.ts
|
|
14797
15848
|
init_dist();
|
|
@@ -15104,7 +16155,9 @@ var DEFAULT_CONFIG = {
|
|
|
15104
16155
|
op: "matches",
|
|
15105
16156
|
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
15106
16157
|
// fails the engine's ReDoS check and the rule would be dropped.
|
|
15107
|
-
|
|
16158
|
+
// `apps`: turning an app off, allowing one of its actions, or
|
|
16159
|
+
// linking a server to an app (the general checks leave it).
|
|
16160
|
+
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",
|
|
15108
16161
|
flags: "i"
|
|
15109
16162
|
}
|
|
15110
16163
|
],
|
|
@@ -15190,7 +16243,12 @@ var DEFAULT_CONFIG = {
|
|
|
15190
16243
|
verdict: "block",
|
|
15191
16244
|
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
15192
16245
|
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."
|
|
15193
|
-
}
|
|
16246
|
+
},
|
|
16247
|
+
// ── Actions in apps (node9Firewall#390) ───────────────────────────────
|
|
16248
|
+
// actions.money / actions.access / actions.publish: the action templates
|
|
16249
|
+
// on every MCP server no app shield claims, plus the shell publishing
|
|
16250
|
+
// rules. Review by default; the check value governs them.
|
|
16251
|
+
...GENERAL_ACTION_RULES
|
|
15194
16252
|
],
|
|
15195
16253
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
|
|
15196
16254
|
egress: {
|
|
@@ -15227,6 +16285,7 @@ var DEFAULT_CONFIG = {
|
|
|
15227
16285
|
trustedHosts: [],
|
|
15228
16286
|
trustedHostsManaged: false,
|
|
15229
16287
|
appPermissions: {},
|
|
16288
|
+
appLinks: {},
|
|
15230
16289
|
managedJailPaths: []
|
|
15231
16290
|
},
|
|
15232
16291
|
environments: {}
|
|
@@ -15461,6 +16520,7 @@ function getConfig(cwd) {
|
|
|
15461
16520
|
trustedHosts: [],
|
|
15462
16521
|
trustedHostsManaged: false,
|
|
15463
16522
|
appPermissions: {},
|
|
16523
|
+
appLinks: {},
|
|
15464
16524
|
managedJailPaths: []
|
|
15465
16525
|
};
|
|
15466
16526
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
@@ -15845,19 +16905,26 @@ function getConfig(cwd) {
|
|
|
15845
16905
|
}
|
|
15846
16906
|
if (Array.isArray(mc.jailPaths)) {
|
|
15847
16907
|
for (const jp of mc.jailPaths) {
|
|
15848
|
-
const
|
|
15849
|
-
if (!
|
|
16908
|
+
const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
16909
|
+
if (!path28) continue;
|
|
15850
16910
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
15851
|
-
for (const r of pathRules(
|
|
16911
|
+
for (const r of pathRules(path28, verdict, "org-managed jail")) {
|
|
15852
16912
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
15853
16913
|
}
|
|
15854
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
16914
|
+
mergedPolicy.managedJailPaths.push({ path: path28, verdict });
|
|
15855
16915
|
}
|
|
15856
16916
|
}
|
|
15857
16917
|
if (Array.isArray(mc.trustedHosts)) {
|
|
15858
16918
|
mergedPolicy.trustedHostsManaged = true;
|
|
15859
16919
|
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
15860
16920
|
}
|
|
16921
|
+
if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
|
|
16922
|
+
const links = {};
|
|
16923
|
+
for (const [key, slug2] of Object.entries(mc.appLinks)) {
|
|
16924
|
+
if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
|
|
16925
|
+
}
|
|
16926
|
+
mergedPolicy.appLinks = links;
|
|
16927
|
+
}
|
|
15861
16928
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
15862
16929
|
const coerced = {};
|
|
15863
16930
|
for (const [srv, tools] of Object.entries(mc.appPermissions)) {
|
|
@@ -16403,7 +17470,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
16403
17470
|
cwd: prepared.cwd,
|
|
16404
17471
|
activeEnvironment,
|
|
16405
17472
|
skipIgnoredFastPath: opts?.skipIgnoredFastPath,
|
|
16406
|
-
argumentScan: opts?.argumentScan
|
|
17473
|
+
argumentScan: opts?.argumentScan,
|
|
17474
|
+
mcpServer: opts?.mcpServer
|
|
16407
17475
|
},
|
|
16408
17476
|
{
|
|
16409
17477
|
checkProvenance,
|
|
@@ -17071,14 +18139,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
|
17071
18139
|
|
|
17072
18140
|
// src/auth/orchestrator.ts
|
|
17073
18141
|
var import_crypto10 = require("crypto");
|
|
17074
|
-
|
|
18142
|
+
|
|
18143
|
+
// src/apps/state.ts
|
|
18144
|
+
var import_fs12 = __toESM(require("fs"));
|
|
18145
|
+
var import_os10 = __toESM(require("os"));
|
|
18146
|
+
var import_path12 = __toESM(require("path"));
|
|
18147
|
+
|
|
18148
|
+
// src/apps/catalog.ts
|
|
18149
|
+
var APP_CATALOG = [
|
|
18150
|
+
{
|
|
18151
|
+
slug: "redis",
|
|
18152
|
+
label: "Redis",
|
|
18153
|
+
shield: "redis",
|
|
18154
|
+
servers: ["redis", "valkey"],
|
|
18155
|
+
commands: ["redis-cli", "valkey-cli"],
|
|
18156
|
+
packages: ["redis", "ioredis", "valkey"],
|
|
18157
|
+
envPrefixes: ["REDIS_"]
|
|
18158
|
+
},
|
|
18159
|
+
{
|
|
18160
|
+
slug: "postgres",
|
|
18161
|
+
label: "PostgreSQL",
|
|
18162
|
+
shield: "postgres",
|
|
18163
|
+
servers: ["postgres", "pgsql"],
|
|
18164
|
+
commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
|
|
18165
|
+
packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
|
|
18166
|
+
envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
|
|
18167
|
+
},
|
|
18168
|
+
{
|
|
18169
|
+
slug: "mongodb",
|
|
18170
|
+
label: "MongoDB",
|
|
18171
|
+
shield: "mongodb",
|
|
18172
|
+
servers: ["mongo"],
|
|
18173
|
+
commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
|
|
18174
|
+
packages: ["mongodb", "mongoose", "pymongo", "motor"],
|
|
18175
|
+
envPrefixes: ["MONGO"]
|
|
18176
|
+
},
|
|
18177
|
+
{
|
|
18178
|
+
slug: "github",
|
|
18179
|
+
label: "GitHub",
|
|
18180
|
+
shield: "github",
|
|
18181
|
+
servers: ["github"],
|
|
18182
|
+
commands: ["gh"],
|
|
18183
|
+
packages: ["@octokit/", "octokit", "pygithub"],
|
|
18184
|
+
envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
|
|
18185
|
+
},
|
|
18186
|
+
{
|
|
18187
|
+
slug: "aws",
|
|
18188
|
+
label: "AWS",
|
|
18189
|
+
shield: "aws",
|
|
18190
|
+
servers: ["aws"],
|
|
18191
|
+
commands: ["aws", "sam", "cdk"],
|
|
18192
|
+
packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
|
|
18193
|
+
envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
|
|
18194
|
+
},
|
|
18195
|
+
{
|
|
18196
|
+
slug: "k8s",
|
|
18197
|
+
label: "Kubernetes",
|
|
18198
|
+
shield: "k8s",
|
|
18199
|
+
servers: ["kubernetes", "k8s", "kubectl"],
|
|
18200
|
+
commands: ["kubectl", "helm", "k9s"],
|
|
18201
|
+
packages: ["@kubernetes/client-node", "kubernetes"],
|
|
18202
|
+
envPrefixes: ["KUBECONFIG"]
|
|
18203
|
+
},
|
|
18204
|
+
{
|
|
18205
|
+
slug: "docker",
|
|
18206
|
+
label: "Docker",
|
|
18207
|
+
shield: "docker",
|
|
18208
|
+
servers: ["docker"],
|
|
18209
|
+
commands: ["docker", "docker-compose", "podman"],
|
|
18210
|
+
packages: ["dockerode", "docker"],
|
|
18211
|
+
envPrefixes: ["DOCKER_HOST"]
|
|
18212
|
+
},
|
|
18213
|
+
{
|
|
18214
|
+
slug: "stripe",
|
|
18215
|
+
label: "Stripe",
|
|
18216
|
+
shield: "stripe",
|
|
18217
|
+
servers: ["stripe"],
|
|
18218
|
+
commands: ["stripe"],
|
|
18219
|
+
packages: ["stripe", "@stripe/"],
|
|
18220
|
+
envPrefixes: ["STRIPE_"]
|
|
18221
|
+
},
|
|
18222
|
+
{
|
|
18223
|
+
slug: "paypal",
|
|
18224
|
+
label: "PayPal",
|
|
18225
|
+
shield: "paypal",
|
|
18226
|
+
servers: ["paypal"],
|
|
18227
|
+
commands: [],
|
|
18228
|
+
packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
|
|
18229
|
+
envPrefixes: ["PAYPAL_"]
|
|
18230
|
+
},
|
|
18231
|
+
{
|
|
18232
|
+
slug: "square",
|
|
18233
|
+
label: "Square",
|
|
18234
|
+
shield: "square",
|
|
18235
|
+
servers: ["square"],
|
|
18236
|
+
// Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
|
|
18237
|
+
notServers: ["squarespace"],
|
|
18238
|
+
commands: [],
|
|
18239
|
+
packages: ["square", "squareup"],
|
|
18240
|
+
envPrefixes: ["SQUARE_"]
|
|
18241
|
+
},
|
|
18242
|
+
{
|
|
18243
|
+
slug: "plaid",
|
|
18244
|
+
label: "Plaid",
|
|
18245
|
+
shield: "plaid",
|
|
18246
|
+
servers: ["plaid"],
|
|
18247
|
+
commands: [],
|
|
18248
|
+
packages: ["plaid", "plaid-python"],
|
|
18249
|
+
envPrefixes: ["PLAID_"]
|
|
18250
|
+
},
|
|
18251
|
+
{
|
|
18252
|
+
slug: "okta",
|
|
18253
|
+
label: "Okta",
|
|
18254
|
+
shield: "okta",
|
|
18255
|
+
servers: ["okta"],
|
|
18256
|
+
commands: ["okta"],
|
|
18257
|
+
packages: ["@okta/", "okta"],
|
|
18258
|
+
envPrefixes: ["OKTA_"]
|
|
18259
|
+
},
|
|
18260
|
+
{
|
|
18261
|
+
slug: "auth0",
|
|
18262
|
+
label: "Auth0",
|
|
18263
|
+
shield: "auth0",
|
|
18264
|
+
servers: ["auth0"],
|
|
18265
|
+
commands: ["auth0"],
|
|
18266
|
+
packages: ["auth0", "@auth0/"],
|
|
18267
|
+
envPrefixes: ["AUTH0_"]
|
|
18268
|
+
}
|
|
18269
|
+
];
|
|
18270
|
+
var APP_BY_SLUG = new Map(
|
|
18271
|
+
APP_CATALOG.map((a) => [a.slug, a])
|
|
18272
|
+
);
|
|
18273
|
+
|
|
18274
|
+
// src/apps/state.ts
|
|
18275
|
+
init_audit();
|
|
18276
|
+
|
|
18277
|
+
// src/daemon/mcp-tools.ts
|
|
18278
|
+
var import_fs11 = __toESM(require("fs"));
|
|
18279
|
+
var import_path11 = __toESM(require("path"));
|
|
18280
|
+
var import_os9 = __toESM(require("os"));
|
|
18281
|
+
function getMcpToolsFile() {
|
|
18282
|
+
return import_path11.default.join(import_os9.default.homedir(), ".node9", "mcp-tools.json");
|
|
18283
|
+
}
|
|
18284
|
+
function readMcpToolsConfig() {
|
|
18285
|
+
try {
|
|
18286
|
+
const file2 = getMcpToolsFile();
|
|
18287
|
+
if (!import_fs11.default.existsSync(file2)) return {};
|
|
18288
|
+
const raw = import_fs11.default.readFileSync(file2, "utf-8");
|
|
18289
|
+
return JSON.parse(raw);
|
|
18290
|
+
} catch {
|
|
18291
|
+
return {};
|
|
18292
|
+
}
|
|
18293
|
+
}
|
|
18294
|
+
|
|
18295
|
+
// src/apps/state.ts
|
|
18296
|
+
function appsFilePath(home = import_os10.default.homedir()) {
|
|
18297
|
+
return import_path12.default.join(home, ".node9", "apps.json");
|
|
18298
|
+
}
|
|
18299
|
+
var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
|
|
18300
|
+
function emptyFile() {
|
|
18301
|
+
return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
|
|
18302
|
+
}
|
|
18303
|
+
function ownString(map, key) {
|
|
18304
|
+
if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
|
|
18305
|
+
const v = map[key];
|
|
18306
|
+
return typeof v === "string" ? v : void 0;
|
|
18307
|
+
}
|
|
18308
|
+
function readSignals(raw) {
|
|
18309
|
+
if (!Array.isArray(raw)) return [];
|
|
18310
|
+
const out = [];
|
|
18311
|
+
for (const x of raw) {
|
|
18312
|
+
if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
|
|
18313
|
+
if (!SIGNAL_KINDS.has(x.kind)) continue;
|
|
18314
|
+
out.push({
|
|
18315
|
+
kind: x.kind,
|
|
18316
|
+
value: x.value.slice(0, 256),
|
|
18317
|
+
...typeof x.project === "string" ? { project: x.project } : {}
|
|
18318
|
+
});
|
|
18319
|
+
}
|
|
18320
|
+
return out;
|
|
18321
|
+
}
|
|
18322
|
+
function parseAppsFile(text) {
|
|
18323
|
+
const raw = JSON.parse(text);
|
|
18324
|
+
const file2 = emptyFile();
|
|
18325
|
+
for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
|
|
18326
|
+
if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
|
|
18327
|
+
file2.apps[slug2] = {
|
|
18328
|
+
state: r.state,
|
|
18329
|
+
by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
|
|
18330
|
+
at: typeof r.at === "string" ? r.at : "",
|
|
18331
|
+
signals: readSignals(r.signals)
|
|
18332
|
+
};
|
|
18333
|
+
}
|
|
18334
|
+
for (const [server, slug2] of Object.entries(raw.links ?? {})) {
|
|
18335
|
+
if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
|
|
18336
|
+
}
|
|
18337
|
+
return file2;
|
|
18338
|
+
}
|
|
18339
|
+
function readAppsFile(home) {
|
|
18340
|
+
return readAppsFileStrict(home) ?? emptyFile();
|
|
18341
|
+
}
|
|
18342
|
+
function readAppsFileStrict(home) {
|
|
18343
|
+
let text;
|
|
18344
|
+
try {
|
|
18345
|
+
text = import_fs12.default.readFileSync(appsFilePath(home), "utf8");
|
|
18346
|
+
} catch (e) {
|
|
18347
|
+
return e.code === "ENOENT" ? emptyFile() : null;
|
|
18348
|
+
}
|
|
18349
|
+
try {
|
|
18350
|
+
return parseAppsFile(text);
|
|
18351
|
+
} catch {
|
|
18352
|
+
return null;
|
|
18353
|
+
}
|
|
18354
|
+
}
|
|
18355
|
+
var linkCache = null;
|
|
18356
|
+
var NO_LINKS = /* @__PURE__ */ Object.create(null);
|
|
18357
|
+
function localLinks() {
|
|
18358
|
+
const file2 = appsFilePath();
|
|
18359
|
+
try {
|
|
18360
|
+
const mtimeMs = import_fs12.default.statSync(file2).mtimeMs;
|
|
18361
|
+
if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
|
|
18362
|
+
linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
|
|
18363
|
+
}
|
|
18364
|
+
return linkCache.links;
|
|
18365
|
+
} catch {
|
|
18366
|
+
return NO_LINKS;
|
|
18367
|
+
}
|
|
18368
|
+
}
|
|
18369
|
+
function resolveLinkedServer(label2, opts = {}) {
|
|
18370
|
+
if (typeof label2 !== "string" || !label2) return label2;
|
|
18371
|
+
const applied = new Set(opts.appliedShields ?? []);
|
|
18372
|
+
const live = (slug2) => {
|
|
18373
|
+
const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
|
|
18374
|
+
return app && applied.has(app.shield) ? slug2 : void 0;
|
|
18375
|
+
};
|
|
18376
|
+
const lower2 = label2.toLowerCase();
|
|
18377
|
+
if (!opts.workspaceGoverned) {
|
|
18378
|
+
const local = live(ownString(localLinks(), lower2));
|
|
18379
|
+
if (local) return local;
|
|
18380
|
+
}
|
|
18381
|
+
const managed = opts.managedLinks ?? {};
|
|
18382
|
+
if (opts.serverKey) {
|
|
18383
|
+
const byKey = live(ownString(managed, opts.serverKey));
|
|
18384
|
+
if (byKey) return byKey;
|
|
18385
|
+
}
|
|
18386
|
+
const keys = Object.keys(managed);
|
|
18387
|
+
if (keys.length === 0) return label2;
|
|
18388
|
+
const tools = readMcpToolsConfig();
|
|
18389
|
+
for (const key of keys) {
|
|
18390
|
+
const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
|
|
18391
|
+
if (typeof name === "string" && name.toLowerCase() === lower2) {
|
|
18392
|
+
const linked = live(ownString(managed, key));
|
|
18393
|
+
if (linked) return linked;
|
|
18394
|
+
}
|
|
18395
|
+
}
|
|
18396
|
+
return label2;
|
|
18397
|
+
}
|
|
18398
|
+
|
|
18399
|
+
// src/auth/orchestrator.ts
|
|
18400
|
+
var import_os20 = __toESM(require("os"));
|
|
17075
18401
|
|
|
17076
18402
|
// src/ui/native.ts
|
|
17077
18403
|
var import_child_process = require("child_process");
|
|
17078
|
-
var
|
|
18404
|
+
var import_path14 = __toESM(require("path"));
|
|
17079
18405
|
|
|
17080
18406
|
// src/context-sniper.ts
|
|
17081
|
-
var
|
|
18407
|
+
var import_path13 = __toESM(require("path"));
|
|
17082
18408
|
function smartTruncate(str, maxLen = 500) {
|
|
17083
18409
|
if (str.length <= maxLen) return str;
|
|
17084
18410
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -17146,7 +18472,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
17146
18472
|
intent = "EDIT";
|
|
17147
18473
|
if (obj.file_path) {
|
|
17148
18474
|
editFilePath = String(obj.file_path);
|
|
17149
|
-
editFileName =
|
|
18475
|
+
editFileName = import_path13.default.basename(editFilePath);
|
|
17150
18476
|
}
|
|
17151
18477
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
17152
18478
|
contextSnippet = result.snippet;
|
|
@@ -17214,7 +18540,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
17214
18540
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17215
18541
|
const obj = parsed;
|
|
17216
18542
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
17217
|
-
const file2 = obj.file_path ?
|
|
18543
|
+
const file2 = obj.file_path ? import_path14.default.basename(String(obj.file_path)) : "file";
|
|
17218
18544
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
17219
18545
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
17220
18546
|
return {
|
|
@@ -17417,24 +18743,24 @@ end run`;
|
|
|
17417
18743
|
}
|
|
17418
18744
|
|
|
17419
18745
|
// src/canary/registry.ts
|
|
17420
|
-
var
|
|
17421
|
-
var
|
|
17422
|
-
var
|
|
18746
|
+
var import_fs14 = __toESM(require("fs"));
|
|
18747
|
+
var import_os12 = __toESM(require("os"));
|
|
18748
|
+
var import_path16 = __toESM(require("path"));
|
|
17423
18749
|
init_dist();
|
|
17424
18750
|
|
|
17425
18751
|
// src/shields/jail.ts
|
|
17426
|
-
var
|
|
17427
|
-
var
|
|
17428
|
-
var
|
|
18752
|
+
var import_fs13 = __toESM(require("fs"));
|
|
18753
|
+
var import_os11 = __toESM(require("os"));
|
|
18754
|
+
var import_path15 = __toESM(require("path"));
|
|
17429
18755
|
init_dist();
|
|
17430
18756
|
var USER_JAIL_SHIELD = "user-jail";
|
|
17431
18757
|
function jailStorePath() {
|
|
17432
|
-
return
|
|
18758
|
+
return import_path15.default.join(import_os11.default.homedir(), ".node9", "jail-paths.json");
|
|
17433
18759
|
}
|
|
17434
18760
|
function readJailPaths() {
|
|
17435
18761
|
let text;
|
|
17436
18762
|
try {
|
|
17437
|
-
text =
|
|
18763
|
+
text = import_fs13.default.readFileSync(jailStorePath(), "utf8");
|
|
17438
18764
|
} catch (err) {
|
|
17439
18765
|
if (err.code === "ENOENT") return [];
|
|
17440
18766
|
throw err;
|
|
@@ -17463,7 +18789,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
17463
18789
|
|
|
17464
18790
|
// src/canary/registry.ts
|
|
17465
18791
|
function canaryStorePath() {
|
|
17466
|
-
return
|
|
18792
|
+
return import_path16.default.join(import_os12.default.homedir(), ".node9", "canaries.json");
|
|
17467
18793
|
}
|
|
17468
18794
|
function isRecord(x) {
|
|
17469
18795
|
if (!x || typeof x !== "object") return false;
|
|
@@ -17474,7 +18800,7 @@ function loadCanaries(opts) {
|
|
|
17474
18800
|
const p = canaryStorePath();
|
|
17475
18801
|
let raw;
|
|
17476
18802
|
try {
|
|
17477
|
-
raw =
|
|
18803
|
+
raw = import_fs14.default.readFileSync(p, "utf-8");
|
|
17478
18804
|
} catch (e) {
|
|
17479
18805
|
if (e.code === "ENOENT") return [];
|
|
17480
18806
|
throw e;
|
|
@@ -17500,22 +18826,22 @@ function canaryValues() {
|
|
|
17500
18826
|
init_dist();
|
|
17501
18827
|
|
|
17502
18828
|
// src/auth/session-script.ts
|
|
17503
|
-
var
|
|
17504
|
-
var
|
|
17505
|
-
var
|
|
18829
|
+
var import_fs15 = __toESM(require("fs"));
|
|
18830
|
+
var import_os13 = __toESM(require("os"));
|
|
18831
|
+
var import_path17 = __toESM(require("path"));
|
|
17506
18832
|
init_dist();
|
|
17507
18833
|
var MAX_SCRIPT_BYTES = 64 * 1024;
|
|
17508
18834
|
var MAX_RECORDED_WRITES = 32;
|
|
17509
18835
|
function readScriptFile(resolved) {
|
|
17510
18836
|
let fd;
|
|
17511
18837
|
try {
|
|
17512
|
-
fd =
|
|
17513
|
-
const st =
|
|
18838
|
+
fd = import_fs15.default.openSync(resolved, import_fs15.default.constants.O_RDONLY | (import_fs15.default.constants.O_NONBLOCK ?? 0));
|
|
18839
|
+
const st = import_fs15.default.fstatSync(fd);
|
|
17514
18840
|
if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
|
|
17515
18841
|
const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
|
|
17516
18842
|
let n = 0;
|
|
17517
18843
|
while (n < buf.length) {
|
|
17518
|
-
const got =
|
|
18844
|
+
const got = import_fs15.default.readSync(fd, buf, n, buf.length - n, n);
|
|
17519
18845
|
if (got === 0) break;
|
|
17520
18846
|
n += got;
|
|
17521
18847
|
}
|
|
@@ -17526,7 +18852,7 @@ function readScriptFile(resolved) {
|
|
|
17526
18852
|
} finally {
|
|
17527
18853
|
if (fd !== void 0) {
|
|
17528
18854
|
try {
|
|
17529
|
-
|
|
18855
|
+
import_fs15.default.closeSync(fd);
|
|
17530
18856
|
} catch {
|
|
17531
18857
|
}
|
|
17532
18858
|
}
|
|
@@ -17534,9 +18860,9 @@ function readScriptFile(resolved) {
|
|
|
17534
18860
|
}
|
|
17535
18861
|
function absolute(p, cwd, home) {
|
|
17536
18862
|
const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
|
|
17537
|
-
return
|
|
18863
|
+
return import_path17.default.resolve(cwd, spelled);
|
|
17538
18864
|
}
|
|
17539
|
-
async function resolveSessionScript(toolName, args, cwd, layers, deps, home =
|
|
18865
|
+
async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os13.default.homedir()) {
|
|
17540
18866
|
try {
|
|
17541
18867
|
const d = deps ?? { checkWritten, readScript: readScriptFile };
|
|
17542
18868
|
const shell = resolveShellInput(toolName, args, layers);
|
|
@@ -17565,7 +18891,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = im
|
|
|
17565
18891
|
return null;
|
|
17566
18892
|
}
|
|
17567
18893
|
}
|
|
17568
|
-
function sessionWriteTargets(toolName, args, fileAccess, cwd, home =
|
|
18894
|
+
function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os13.default.homedir()) {
|
|
17569
18895
|
const base2 = cwd ?? process.cwd();
|
|
17570
18896
|
const out = /* @__PURE__ */ new Set();
|
|
17571
18897
|
const argsObj = args && typeof args === "object" ? args : {};
|
|
@@ -17595,26 +18921,26 @@ init_audit();
|
|
|
17595
18921
|
init_dist();
|
|
17596
18922
|
|
|
17597
18923
|
// src/exposure/known-hosts.ts
|
|
17598
|
-
var
|
|
17599
|
-
var
|
|
18924
|
+
var import_fs17 = __toESM(require("fs"));
|
|
18925
|
+
var import_path19 = __toESM(require("path"));
|
|
17600
18926
|
|
|
17601
18927
|
// src/utils/git-remotes.ts
|
|
17602
|
-
var
|
|
17603
|
-
var
|
|
18928
|
+
var import_fs16 = __toESM(require("fs"));
|
|
18929
|
+
var import_path18 = __toESM(require("path"));
|
|
17604
18930
|
init_dist();
|
|
17605
18931
|
function readSmallFile(p, max) {
|
|
17606
18932
|
let fd;
|
|
17607
18933
|
try {
|
|
17608
|
-
fd =
|
|
17609
|
-
const st =
|
|
18934
|
+
fd = import_fs16.default.openSync(p, import_fs16.default.constants.O_RDONLY | (import_fs16.default.constants.O_NOFOLLOW ?? 0));
|
|
18935
|
+
const st = import_fs16.default.fstatSync(fd);
|
|
17610
18936
|
if (!st.isFile() || st.size > max) return null;
|
|
17611
18937
|
const buf = Buffer.alloc(st.size);
|
|
17612
|
-
const n =
|
|
18938
|
+
const n = import_fs16.default.readSync(fd, buf, 0, st.size, 0);
|
|
17613
18939
|
return buf.subarray(0, n).toString("utf8");
|
|
17614
18940
|
} catch {
|
|
17615
18941
|
return null;
|
|
17616
18942
|
} finally {
|
|
17617
|
-
if (fd !== void 0)
|
|
18943
|
+
if (fd !== void 0) import_fs16.default.closeSync(fd);
|
|
17618
18944
|
}
|
|
17619
18945
|
}
|
|
17620
18946
|
function gitRemoteHost(url) {
|
|
@@ -17633,20 +18959,20 @@ function gitRemoteHost(url) {
|
|
|
17633
18959
|
return null;
|
|
17634
18960
|
}
|
|
17635
18961
|
function findGitConfig(dir2, walkUp = true) {
|
|
17636
|
-
let cur =
|
|
18962
|
+
let cur = import_path18.default.resolve(dir2);
|
|
17637
18963
|
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
17638
|
-
const dotGit =
|
|
17639
|
-
const direct = readSmallFile(
|
|
18964
|
+
const dotGit = import_path18.default.join(cur, ".git");
|
|
18965
|
+
const direct = readSmallFile(import_path18.default.join(dotGit, "config"), 1e6);
|
|
17640
18966
|
if (direct !== null) return direct;
|
|
17641
18967
|
const pointer = readSmallFile(dotGit, 4096);
|
|
17642
18968
|
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
17643
18969
|
if (m) {
|
|
17644
|
-
let gitDir =
|
|
17645
|
-
const common = readSmallFile(
|
|
17646
|
-
if (common !== null) gitDir =
|
|
17647
|
-
return readSmallFile(
|
|
18970
|
+
let gitDir = import_path18.default.resolve(cur, m[1]);
|
|
18971
|
+
const common = readSmallFile(import_path18.default.join(gitDir, "commondir"), 4096);
|
|
18972
|
+
if (common !== null) gitDir = import_path18.default.resolve(gitDir, common.trim());
|
|
18973
|
+
return readSmallFile(import_path18.default.join(gitDir, "config"), 1e6);
|
|
17648
18974
|
}
|
|
17649
|
-
const parent =
|
|
18975
|
+
const parent = import_path18.default.dirname(cur);
|
|
17650
18976
|
if (parent === cur) return null;
|
|
17651
18977
|
cur = parent;
|
|
17652
18978
|
}
|
|
@@ -17656,12 +18982,12 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
17656
18982
|
function readGitRemotes(dir2, opts = {}) {
|
|
17657
18983
|
if (!dir2) return [];
|
|
17658
18984
|
const walkUp = opts.walkUp !== false;
|
|
17659
|
-
const key = `${walkUp ? "up" : "at"}:${
|
|
18985
|
+
const key = `${walkUp ? "up" : "at"}:${import_path18.default.resolve(dir2)}`;
|
|
17660
18986
|
const hit = cache.get(key);
|
|
17661
18987
|
if (hit) return hit;
|
|
17662
18988
|
const out = [];
|
|
17663
18989
|
try {
|
|
17664
|
-
const cfg = findGitConfig(
|
|
18990
|
+
const cfg = findGitConfig(import_path18.default.resolve(dir2), walkUp);
|
|
17665
18991
|
if (cfg !== null) {
|
|
17666
18992
|
let current = null;
|
|
17667
18993
|
for (const line of cfg.split(/\r?\n/)) {
|
|
@@ -17697,15 +19023,15 @@ var ECOSYSTEMS = [
|
|
|
17697
19023
|
];
|
|
17698
19024
|
var cache2 = /* @__PURE__ */ new Map();
|
|
17699
19025
|
function manifestRoot(dir2) {
|
|
17700
|
-
let cur =
|
|
19026
|
+
let cur = import_path19.default.resolve(dir2);
|
|
17701
19027
|
for (let i = 0; i < 64; i++) {
|
|
17702
19028
|
for (const [m] of ECOSYSTEMS) {
|
|
17703
19029
|
try {
|
|
17704
|
-
if (
|
|
19030
|
+
if (import_fs17.default.existsSync(import_path19.default.join(cur, m))) return cur;
|
|
17705
19031
|
} catch {
|
|
17706
19032
|
}
|
|
17707
19033
|
}
|
|
17708
|
-
const parent =
|
|
19034
|
+
const parent = import_path19.default.dirname(cur);
|
|
17709
19035
|
if (parent === cur) return null;
|
|
17710
19036
|
cur = parent;
|
|
17711
19037
|
}
|
|
@@ -17714,7 +19040,7 @@ function manifestRoot(dir2) {
|
|
|
17714
19040
|
function knownProjectHosts(dir2) {
|
|
17715
19041
|
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
17716
19042
|
if (!dir2) return empty;
|
|
17717
|
-
const key =
|
|
19043
|
+
const key = import_path19.default.resolve(dir2);
|
|
17718
19044
|
const hit = cache2.get(key);
|
|
17719
19045
|
if (hit) return hit;
|
|
17720
19046
|
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
@@ -17727,10 +19053,10 @@ function knownProjectHosts(dir2) {
|
|
|
17727
19053
|
const root = manifestRoot(key);
|
|
17728
19054
|
if (root !== null) {
|
|
17729
19055
|
for (const [m, registry] of ECOSYSTEMS) {
|
|
17730
|
-
if (
|
|
19056
|
+
if (import_fs17.default.existsSync(import_path19.default.join(root, m))) out.hosts.add(registry);
|
|
17731
19057
|
}
|
|
17732
19058
|
try {
|
|
17733
|
-
if (
|
|
19059
|
+
if (import_fs17.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
17734
19060
|
out.hosts.add("host:rubygems.org");
|
|
17735
19061
|
} catch {
|
|
17736
19062
|
}
|
|
@@ -17742,9 +19068,9 @@ function knownProjectHosts(dir2) {
|
|
|
17742
19068
|
}
|
|
17743
19069
|
|
|
17744
19070
|
// src/exposure/writes.ts
|
|
17745
|
-
var
|
|
17746
|
-
var
|
|
17747
|
-
var
|
|
19071
|
+
var import_fs18 = __toESM(require("fs"));
|
|
19072
|
+
var import_os14 = __toESM(require("os"));
|
|
19073
|
+
var import_path20 = __toESM(require("path"));
|
|
17748
19074
|
init_dist();
|
|
17749
19075
|
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
17750
19076
|
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
@@ -17774,7 +19100,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17774
19100
|
if (typeof a.content === "string") {
|
|
17775
19101
|
try {
|
|
17776
19102
|
const next = JSON.parse(a.content);
|
|
17777
|
-
const prev = JSON.parse(
|
|
19103
|
+
const prev = JSON.parse(import_fs18.default.readFileSync(absPath, "utf8"));
|
|
17778
19104
|
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
17779
19105
|
} catch {
|
|
17780
19106
|
return LIFECYCLE_RE.test(a.content);
|
|
@@ -17784,8 +19110,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17784
19110
|
}
|
|
17785
19111
|
function judgeWritesAfterExposure(opts) {
|
|
17786
19112
|
const holds = [];
|
|
17787
|
-
const home = opts.home ??
|
|
17788
|
-
const tmp = normalizeFilePath(
|
|
19113
|
+
const home = opts.home ?? import_os14.default.homedir();
|
|
19114
|
+
const tmp = normalizeFilePath(import_os14.default.tmpdir());
|
|
17789
19115
|
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
17790
19116
|
const seen = /* @__PURE__ */ new Set();
|
|
17791
19117
|
for (const access of opts.accesses) {
|
|
@@ -17795,7 +19121,7 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17795
19121
|
seen.add(p);
|
|
17796
19122
|
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
17797
19123
|
if (root !== null && isInside(root, p)) {
|
|
17798
|
-
if (isAgentInstructionPath(p, root) && (
|
|
19124
|
+
if (isAgentInstructionPath(p, root) && (import_path20.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
17799
19125
|
holds.push({ path: p, why: "agent-instruction" });
|
|
17800
19126
|
continue;
|
|
17801
19127
|
}
|
|
@@ -17817,25 +19143,25 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17817
19143
|
|
|
17818
19144
|
// src/exposure/marker.ts
|
|
17819
19145
|
var import_crypto8 = __toESM(require("crypto"));
|
|
17820
|
-
var
|
|
17821
|
-
var
|
|
17822
|
-
var
|
|
19146
|
+
var import_fs19 = __toESM(require("fs"));
|
|
19147
|
+
var import_os15 = __toESM(require("os"));
|
|
19148
|
+
var import_path21 = __toESM(require("path"));
|
|
17823
19149
|
|
|
17824
19150
|
// src/daemon/exposure-store.ts
|
|
17825
19151
|
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
17826
19152
|
|
|
17827
19153
|
// src/exposure/marker.ts
|
|
17828
19154
|
function dir() {
|
|
17829
|
-
return
|
|
19155
|
+
return import_path21.default.join(import_os15.default.homedir(), ".node9", "exposure-sessions");
|
|
17830
19156
|
}
|
|
17831
19157
|
function file(sessionId) {
|
|
17832
19158
|
const id = import_crypto8.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
17833
|
-
return
|
|
19159
|
+
return import_path21.default.join(dir(), id);
|
|
17834
19160
|
}
|
|
17835
19161
|
function maybeSessionExposed(sessionId) {
|
|
17836
19162
|
if (!sessionId) return false;
|
|
17837
19163
|
try {
|
|
17838
|
-
const st =
|
|
19164
|
+
const st = import_fs19.default.statSync(file(sessionId));
|
|
17839
19165
|
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
17840
19166
|
} catch (e) {
|
|
17841
19167
|
return e.code !== "ENOENT";
|
|
@@ -17843,27 +19169,7 @@ function maybeSessionExposed(sessionId) {
|
|
|
17843
19169
|
}
|
|
17844
19170
|
|
|
17845
19171
|
// src/exposure/roles.ts
|
|
17846
|
-
var
|
|
17847
|
-
|
|
17848
|
-
// src/daemon/mcp-tools.ts
|
|
17849
|
-
var import_fs18 = __toESM(require("fs"));
|
|
17850
|
-
var import_path20 = __toESM(require("path"));
|
|
17851
|
-
var import_os14 = __toESM(require("os"));
|
|
17852
|
-
function getMcpToolsFile() {
|
|
17853
|
-
return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
|
|
17854
|
-
}
|
|
17855
|
-
function readMcpToolsConfig() {
|
|
17856
|
-
try {
|
|
17857
|
-
const file2 = getMcpToolsFile();
|
|
17858
|
-
if (!import_fs18.default.existsSync(file2)) return {};
|
|
17859
|
-
const raw = import_fs18.default.readFileSync(file2, "utf-8");
|
|
17860
|
-
return JSON.parse(raw);
|
|
17861
|
-
} catch {
|
|
17862
|
-
return {};
|
|
17863
|
-
}
|
|
17864
|
-
}
|
|
17865
|
-
|
|
17866
|
-
// src/exposure/roles.ts
|
|
19172
|
+
var import_fs20 = __toESM(require("fs"));
|
|
17867
19173
|
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
17868
19174
|
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
17869
19175
|
function foldServerName(name) {
|
|
@@ -17873,7 +19179,7 @@ var inventoryCache = null;
|
|
|
17873
19179
|
var inventoryStamp = null;
|
|
17874
19180
|
function inventoryFileStamp() {
|
|
17875
19181
|
try {
|
|
17876
|
-
const st =
|
|
19182
|
+
const st = import_fs20.default.statSync(getMcpToolsFile());
|
|
17877
19183
|
return `${st.mtimeMs}:${st.size}`;
|
|
17878
19184
|
} catch {
|
|
17879
19185
|
return "";
|
|
@@ -17957,16 +19263,16 @@ function sourceWord(toolName) {
|
|
|
17957
19263
|
}
|
|
17958
19264
|
|
|
17959
19265
|
// src/auth/trusted-recipients.ts
|
|
17960
|
-
var
|
|
17961
|
-
var
|
|
17962
|
-
var
|
|
19266
|
+
var import_fs21 = __toESM(require("fs"));
|
|
19267
|
+
var import_path22 = __toESM(require("path"));
|
|
19268
|
+
var import_os16 = __toESM(require("os"));
|
|
17963
19269
|
init_dist();
|
|
17964
19270
|
function getTrustedRecipientsPath() {
|
|
17965
|
-
return
|
|
19271
|
+
return import_path22.default.join(import_os16.default.homedir(), ".node9", "trusted-recipients.json");
|
|
17966
19272
|
}
|
|
17967
19273
|
function readTrustedRecipients() {
|
|
17968
19274
|
try {
|
|
17969
|
-
const raw =
|
|
19275
|
+
const raw = import_fs21.default.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
17970
19276
|
const parsed = JSON.parse(raw);
|
|
17971
19277
|
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
17972
19278
|
} catch {
|
|
@@ -17975,10 +19281,10 @@ function readTrustedRecipients() {
|
|
|
17975
19281
|
}
|
|
17976
19282
|
function writeTrustedRecipients(entries) {
|
|
17977
19283
|
const filePath = getTrustedRecipientsPath();
|
|
17978
|
-
|
|
19284
|
+
import_fs21.default.mkdirSync(import_path22.default.dirname(filePath), { recursive: true });
|
|
17979
19285
|
const tmp = filePath + ".node9-tmp";
|
|
17980
|
-
|
|
17981
|
-
|
|
19286
|
+
import_fs21.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
19287
|
+
import_fs21.default.renameSync(tmp, filePath);
|
|
17982
19288
|
}
|
|
17983
19289
|
function addTrustedRecipient(raw, addedBy = "user") {
|
|
17984
19290
|
const value = normalizeRecipient(raw);
|
|
@@ -17993,7 +19299,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
|
|
|
17993
19299
|
var cache3 = null;
|
|
17994
19300
|
function mtimeOf() {
|
|
17995
19301
|
try {
|
|
17996
|
-
return
|
|
19302
|
+
return import_fs21.default.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
17997
19303
|
} catch {
|
|
17998
19304
|
return 0;
|
|
17999
19305
|
}
|
|
@@ -18007,41 +19313,41 @@ function isTrustedRecipient(value) {
|
|
|
18007
19313
|
}
|
|
18008
19314
|
|
|
18009
19315
|
// src/auth/cloud.ts
|
|
18010
|
-
var
|
|
18011
|
-
var
|
|
18012
|
-
var
|
|
19316
|
+
var import_fs23 = __toESM(require("fs"));
|
|
19317
|
+
var import_os17 = __toESM(require("os"));
|
|
19318
|
+
var import_path23 = __toESM(require("path"));
|
|
18013
19319
|
init_audit();
|
|
18014
19320
|
|
|
18015
19321
|
// src/utils/read-capped.ts
|
|
18016
|
-
var
|
|
19322
|
+
var import_fs22 = __toESM(require("fs"));
|
|
18017
19323
|
function readCapped(file2, maxBytes) {
|
|
18018
19324
|
if (maxBytes < 0) return null;
|
|
18019
19325
|
let fd;
|
|
18020
19326
|
try {
|
|
18021
|
-
fd =
|
|
19327
|
+
fd = import_fs22.default.openSync(file2, "r");
|
|
18022
19328
|
} catch {
|
|
18023
19329
|
return null;
|
|
18024
19330
|
}
|
|
18025
19331
|
try {
|
|
18026
|
-
const st =
|
|
19332
|
+
const st = import_fs22.default.fstatSync(fd);
|
|
18027
19333
|
if (!st.isFile()) return null;
|
|
18028
19334
|
const want = Math.min(st.size, maxBytes + 1);
|
|
18029
19335
|
const buf = Buffer.alloc(want);
|
|
18030
|
-
let
|
|
18031
|
-
while (
|
|
18032
|
-
const n =
|
|
19336
|
+
let read2 = 0;
|
|
19337
|
+
while (read2 < want) {
|
|
19338
|
+
const n = import_fs22.default.readSync(fd, buf, read2, want - read2, read2);
|
|
18033
19339
|
if (n <= 0) break;
|
|
18034
|
-
|
|
19340
|
+
read2 += n;
|
|
18035
19341
|
}
|
|
18036
19342
|
return {
|
|
18037
|
-
bytes: buf.subarray(0, Math.min(
|
|
18038
|
-
truncated:
|
|
19343
|
+
bytes: buf.subarray(0, Math.min(read2, maxBytes)),
|
|
19344
|
+
truncated: read2 > maxBytes
|
|
18039
19345
|
};
|
|
18040
19346
|
} catch {
|
|
18041
19347
|
return null;
|
|
18042
19348
|
} finally {
|
|
18043
19349
|
try {
|
|
18044
|
-
|
|
19350
|
+
import_fs22.default.closeSync(fd);
|
|
18045
19351
|
} catch {
|
|
18046
19352
|
}
|
|
18047
19353
|
}
|
|
@@ -18086,7 +19392,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18086
19392
|
let ciContext;
|
|
18087
19393
|
if (process.env.CI) {
|
|
18088
19394
|
try {
|
|
18089
|
-
const ciContextPath =
|
|
19395
|
+
const ciContextPath = import_path23.default.join(import_os17.default.homedir(), ".node9", "ci-context.json");
|
|
18090
19396
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
18091
19397
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
18092
19398
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -18128,9 +19434,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18128
19434
|
context: {
|
|
18129
19435
|
agent: meta?.agent,
|
|
18130
19436
|
mcpServer: meta?.mcpServer,
|
|
18131
|
-
hostname:
|
|
19437
|
+
hostname: import_os17.default.hostname(),
|
|
18132
19438
|
cwd: process.cwd(),
|
|
18133
|
-
platform:
|
|
19439
|
+
platform: import_os17.default.platform()
|
|
18134
19440
|
},
|
|
18135
19441
|
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
18136
19442
|
// an unknown key, so only fields its schema declares go on the wire.
|
|
@@ -18204,14 +19510,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18204
19510
|
});
|
|
18205
19511
|
clearTimeout(timer);
|
|
18206
19512
|
if (!res.ok) {
|
|
18207
|
-
|
|
19513
|
+
import_fs23.default.appendFileSync(
|
|
18208
19514
|
HOOK_DEBUG_LOG,
|
|
18209
19515
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
18210
19516
|
`
|
|
18211
19517
|
);
|
|
18212
19518
|
}
|
|
18213
19519
|
} catch (err) {
|
|
18214
|
-
|
|
19520
|
+
import_fs23.default.appendFileSync(
|
|
18215
19521
|
HOOK_DEBUG_LOG,
|
|
18216
19522
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
18217
19523
|
`
|
|
@@ -18220,18 +19526,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18220
19526
|
}
|
|
18221
19527
|
|
|
18222
19528
|
// src/loop-detector.ts
|
|
18223
|
-
var
|
|
18224
|
-
var
|
|
18225
|
-
var
|
|
19529
|
+
var import_fs24 = __toESM(require("fs"));
|
|
19530
|
+
var import_path24 = __toESM(require("path"));
|
|
19531
|
+
var import_os18 = __toESM(require("os"));
|
|
18226
19532
|
init_dist();
|
|
18227
19533
|
init_dist();
|
|
18228
19534
|
function loopStateFile() {
|
|
18229
|
-
return
|
|
19535
|
+
return import_path24.default.join(import_os18.default.homedir(), ".node9", "loop-state.json");
|
|
18230
19536
|
}
|
|
18231
19537
|
function readState() {
|
|
18232
19538
|
try {
|
|
18233
|
-
if (!
|
|
18234
|
-
const raw =
|
|
19539
|
+
if (!import_fs24.default.existsSync(loopStateFile())) return [];
|
|
19540
|
+
const raw = import_fs24.default.readFileSync(loopStateFile(), "utf-8");
|
|
18235
19541
|
const parsed = JSON.parse(raw);
|
|
18236
19542
|
if (!Array.isArray(parsed)) return [];
|
|
18237
19543
|
return parsed;
|
|
@@ -18240,11 +19546,11 @@ function readState() {
|
|
|
18240
19546
|
}
|
|
18241
19547
|
}
|
|
18242
19548
|
function writeState(records) {
|
|
18243
|
-
const dir2 =
|
|
18244
|
-
if (!
|
|
18245
|
-
const tmpPath = `${loopStateFile()}.${
|
|
18246
|
-
|
|
18247
|
-
|
|
19549
|
+
const dir2 = import_path24.default.dirname(loopStateFile());
|
|
19550
|
+
if (!import_fs24.default.existsSync(dir2)) import_fs24.default.mkdirSync(dir2, { recursive: true });
|
|
19551
|
+
const tmpPath = `${loopStateFile()}.${import_os18.default.hostname()}.${process.pid}.tmp`;
|
|
19552
|
+
import_fs24.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
19553
|
+
import_fs24.default.renameSync(tmpPath, loopStateFile());
|
|
18248
19554
|
}
|
|
18249
19555
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
18250
19556
|
try {
|
|
@@ -18258,21 +19564,21 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
18258
19564
|
|
|
18259
19565
|
// src/supply-chain/check.ts
|
|
18260
19566
|
var import_picomatch3 = __toESM(require("picomatch"));
|
|
18261
|
-
var
|
|
19567
|
+
var import_path27 = __toESM(require("path"));
|
|
18262
19568
|
init_dist();
|
|
18263
19569
|
|
|
18264
19570
|
// src/supply-chain/osv-index.ts
|
|
18265
|
-
var
|
|
18266
|
-
var
|
|
18267
|
-
var
|
|
19571
|
+
var import_fs25 = __toESM(require("fs"));
|
|
19572
|
+
var import_os19 = __toESM(require("os"));
|
|
19573
|
+
var import_path25 = __toESM(require("path"));
|
|
18268
19574
|
var import_crypto9 = require("crypto");
|
|
18269
19575
|
init_dist();
|
|
18270
19576
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
18271
19577
|
function osvRoot() {
|
|
18272
|
-
return
|
|
19578
|
+
return import_path25.default.join(import_os19.default.homedir(), ".node9", "osv");
|
|
18273
19579
|
}
|
|
18274
19580
|
function ecoDir(eco) {
|
|
18275
|
-
return
|
|
19581
|
+
return import_path25.default.join(osvRoot(), eco);
|
|
18276
19582
|
}
|
|
18277
19583
|
function indexKey(eco, name) {
|
|
18278
19584
|
switch (eco) {
|
|
@@ -18291,11 +19597,11 @@ function shardOf(key) {
|
|
|
18291
19597
|
return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
|
|
18292
19598
|
}
|
|
18293
19599
|
function shardPath(eco, shard) {
|
|
18294
|
-
return
|
|
19600
|
+
return import_path25.default.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
18295
19601
|
}
|
|
18296
19602
|
function readMeta(eco) {
|
|
18297
19603
|
try {
|
|
18298
|
-
const m = JSON.parse(
|
|
19604
|
+
const m = JSON.parse(import_fs25.default.readFileSync(import_path25.default.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
18299
19605
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
18300
19606
|
return m;
|
|
18301
19607
|
} catch {
|
|
@@ -18304,7 +19610,7 @@ function readMeta(eco) {
|
|
|
18304
19610
|
}
|
|
18305
19611
|
function readShard(eco, shard) {
|
|
18306
19612
|
try {
|
|
18307
|
-
const s = JSON.parse(
|
|
19613
|
+
const s = JSON.parse(import_fs25.default.readFileSync(shardPath(eco, shard), "utf8"));
|
|
18308
19614
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
18309
19615
|
} catch {
|
|
18310
19616
|
return {};
|
|
@@ -18430,14 +19736,102 @@ async function queryOsvMalicious(queries) {
|
|
|
18430
19736
|
}
|
|
18431
19737
|
|
|
18432
19738
|
// src/supply-chain/registry.ts
|
|
19739
|
+
var import_semver2 = require("semver");
|
|
19740
|
+
|
|
19741
|
+
// src/supply-chain/provenance.ts
|
|
18433
19742
|
var import_semver = require("semver");
|
|
18434
19743
|
var TIMEOUT_MS2 = 1500;
|
|
18435
|
-
var MAX_BYTES =
|
|
19744
|
+
var MAX_BYTES = 256 * 1024;
|
|
19745
|
+
function normalizeRepo(raw, host2 = "github.com") {
|
|
19746
|
+
let s = raw.trim().toLowerCase();
|
|
19747
|
+
s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
|
|
19748
|
+
s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
|
|
19749
|
+
if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
|
|
19750
|
+
return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
|
|
19751
|
+
}
|
|
19752
|
+
function npmRepoOf(doc) {
|
|
19753
|
+
const list = doc?.attestations;
|
|
19754
|
+
if (!Array.isArray(list)) return null;
|
|
19755
|
+
for (const a of list) {
|
|
19756
|
+
const att = a;
|
|
19757
|
+
if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
|
|
19758
|
+
const payload = att.bundle?.dsseEnvelope?.payload;
|
|
19759
|
+
if (typeof payload !== "string") continue;
|
|
19760
|
+
try {
|
|
19761
|
+
const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
|
19762
|
+
const p = statement.predicate;
|
|
19763
|
+
const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
|
|
19764
|
+
if (typeof repo === "string") return normalizeRepo(repo);
|
|
19765
|
+
} catch {
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
return null;
|
|
19769
|
+
}
|
|
19770
|
+
function pypiRepoOf(doc) {
|
|
19771
|
+
const bundles = doc?.attestation_bundles;
|
|
19772
|
+
if (!Array.isArray(bundles)) return null;
|
|
19773
|
+
for (const b of bundles) {
|
|
19774
|
+
const pub = b.publisher;
|
|
19775
|
+
if (typeof pub?.repository === "string") {
|
|
19776
|
+
const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
|
|
19777
|
+
return normalizeRepo(pub.repository, host2);
|
|
19778
|
+
}
|
|
19779
|
+
if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
|
|
19780
|
+
return `google:${pub.email.trim().toLowerCase()}`;
|
|
19781
|
+
}
|
|
19782
|
+
}
|
|
19783
|
+
return null;
|
|
19784
|
+
}
|
|
19785
|
+
async function npmSourceRepo(name, version) {
|
|
19786
|
+
const base2 = endpoints().npmRegistry;
|
|
19787
|
+
if (!base2) return "unknown";
|
|
19788
|
+
const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
|
|
19789
|
+
return read(url, npmRepoOf);
|
|
19790
|
+
}
|
|
19791
|
+
async function read(url, repoOf) {
|
|
19792
|
+
try {
|
|
19793
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
|
|
19794
|
+
if (doc === null) return "none";
|
|
19795
|
+
return repoOf(doc) ?? "unknown";
|
|
19796
|
+
} catch {
|
|
19797
|
+
return "unknown";
|
|
19798
|
+
}
|
|
19799
|
+
}
|
|
19800
|
+
async function pypiSourceRepo(name, version, filename) {
|
|
19801
|
+
const base2 = endpoints().pypi;
|
|
19802
|
+
if (!base2) return "unknown";
|
|
19803
|
+
const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
|
|
19804
|
+
return read(url, pypiRepoOf);
|
|
19805
|
+
}
|
|
19806
|
+
var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
|
|
19807
|
+
function comparePypi(a, b) {
|
|
19808
|
+
const x = a.split(".").map(Number);
|
|
19809
|
+
const y = b.split(".").map(Number);
|
|
19810
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
19811
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
19812
|
+
if (d !== 0) return d;
|
|
19813
|
+
}
|
|
19814
|
+
return 0;
|
|
19815
|
+
}
|
|
19816
|
+
function previousRelease(eco, versions, version) {
|
|
19817
|
+
if (eco === "npm") {
|
|
19818
|
+
if (!(0, import_semver.valid)(version)) return void 0;
|
|
19819
|
+
const below2 = versions.filter((v) => (0, import_semver.valid)(v) && !(0, import_semver.prerelease)(v) && (0, import_semver.rcompare)(v, version) > 0);
|
|
19820
|
+
return below2.sort(import_semver.rcompare)[0] ?? null;
|
|
19821
|
+
}
|
|
19822
|
+
if (!PYPI_FINAL.test(version)) return void 0;
|
|
19823
|
+
const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
|
|
19824
|
+
return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
|
|
19825
|
+
}
|
|
19826
|
+
|
|
19827
|
+
// src/supply-chain/registry.ts
|
|
19828
|
+
var TIMEOUT_MS3 = 1500;
|
|
19829
|
+
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
18436
19830
|
var VERSION_DOC_MAX_BYTES = 256 * 1024;
|
|
18437
19831
|
var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
18438
19832
|
async function npmVersionDocument(url, selector) {
|
|
18439
19833
|
const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
|
|
18440
|
-
timeoutMs:
|
|
19834
|
+
timeoutMs: TIMEOUT_MS3,
|
|
18441
19835
|
maxBytes: VERSION_DOC_MAX_BYTES
|
|
18442
19836
|
});
|
|
18443
19837
|
if (!doc?.version) return null;
|
|
@@ -18461,16 +19855,16 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18461
19855
|
let doc;
|
|
18462
19856
|
try {
|
|
18463
19857
|
doc = await fetchJson(url, {
|
|
18464
|
-
timeoutMs:
|
|
18465
|
-
maxBytes:
|
|
19858
|
+
timeoutMs: TIMEOUT_MS3,
|
|
19859
|
+
maxBytes: MAX_BYTES2,
|
|
18466
19860
|
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
18467
19861
|
});
|
|
18468
19862
|
} catch (err) {
|
|
18469
19863
|
if (!/too large/.test(err.message)) throw err;
|
|
18470
|
-
const range = !version && spec && (0,
|
|
19864
|
+
const range = !version && spec && (0, import_semver2.validRange)(spec) ? spec : null;
|
|
18471
19865
|
const selector = version ?? (spec && !range ? spec : "latest");
|
|
18472
19866
|
const info2 = await npmVersionDocument(url, selector);
|
|
18473
|
-
if (info2 && range && !(0,
|
|
19867
|
+
if (info2 && range && !(0, import_semver2.satisfies)(info2.version, range)) return null;
|
|
18474
19868
|
return info2;
|
|
18475
19869
|
}
|
|
18476
19870
|
if (!doc) return null;
|
|
@@ -18479,8 +19873,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18479
19873
|
if (!resolved) {
|
|
18480
19874
|
if (!spec) resolved = latest;
|
|
18481
19875
|
else if (doc["dist-tags"]?.[spec]) resolved = doc["dist-tags"][spec];
|
|
18482
|
-
else if ((0,
|
|
18483
|
-
resolved = latest && (0,
|
|
19876
|
+
else if ((0, import_semver2.validRange)(spec)) {
|
|
19877
|
+
resolved = latest && (0, import_semver2.satisfies)(latest, spec) ? latest : (0, import_semver2.maxSatisfying)(Object.keys(doc.versions ?? {}), spec) ?? void 0;
|
|
18484
19878
|
}
|
|
18485
19879
|
}
|
|
18486
19880
|
if (!resolved) return null;
|
|
@@ -18490,13 +19884,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18490
19884
|
version: resolved,
|
|
18491
19885
|
hasInstallScript: v?.hasInstallScript === true
|
|
18492
19886
|
};
|
|
19887
|
+
const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
|
|
19888
|
+
if (v && previous !== void 0) {
|
|
19889
|
+
const pv = previous ? doc.versions?.[previous] : void 0;
|
|
19890
|
+
info.history = {
|
|
19891
|
+
previous,
|
|
19892
|
+
previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
|
|
19893
|
+
provenance: !!v.dist?.attestations?.provenance,
|
|
19894
|
+
previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
|
|
19895
|
+
};
|
|
19896
|
+
}
|
|
18493
19897
|
if (!Number.isFinite(modifiedMs)) return info;
|
|
18494
19898
|
if (Date.now() - modifiedMs >= freshWindowMs) {
|
|
18495
19899
|
info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
|
|
18496
19900
|
return info;
|
|
18497
19901
|
}
|
|
18498
19902
|
try {
|
|
18499
|
-
const full = await fetchJson(url, { timeoutMs:
|
|
19903
|
+
const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18500
19904
|
const t = full?.time?.[resolved];
|
|
18501
19905
|
const tMs = t ? Date.parse(t) : NaN;
|
|
18502
19906
|
if (Number.isFinite(tMs)) {
|
|
@@ -18514,15 +19918,37 @@ async function pypiInfo(name, version) {
|
|
|
18514
19918
|
const base2 = endpoints().pypi;
|
|
18515
19919
|
if (!base2) return null;
|
|
18516
19920
|
const url = version ? `${base2}/pypi/${encodeURIComponent(name)}/${encodeURIComponent(version)}/json` : `${base2}/pypi/${encodeURIComponent(name)}/json`;
|
|
18517
|
-
const doc = await fetchJson(url, { timeoutMs:
|
|
19921
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18518
19922
|
if (!doc?.info?.version) return null;
|
|
18519
19923
|
const times = (doc.urls ?? []).map((u) => u.upload_time_iso_8601 ? Date.parse(u.upload_time_iso_8601) : NaN).filter((t) => Number.isFinite(t));
|
|
18520
|
-
|
|
18521
|
-
|
|
19924
|
+
const resolved = version ?? doc.info.version;
|
|
19925
|
+
const info = {
|
|
19926
|
+
version: resolved,
|
|
18522
19927
|
// The earliest file of the release is when it became installable.
|
|
18523
|
-
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
|
|
19928
|
+
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
|
|
19929
|
+
file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
|
|
19930
|
+
};
|
|
19931
|
+
if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
|
|
19932
|
+
return info;
|
|
19933
|
+
}
|
|
19934
|
+
function pypiHistoryOf(releases, version) {
|
|
19935
|
+
const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
|
|
19936
|
+
const previous = previousRelease("PyPI", versions, version);
|
|
19937
|
+
if (previous === void 0) return null;
|
|
19938
|
+
return {
|
|
19939
|
+
previous,
|
|
19940
|
+
previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
|
|
18524
19941
|
};
|
|
18525
19942
|
}
|
|
19943
|
+
async function pypiHistory(name, version) {
|
|
19944
|
+
const base2 = endpoints().pypi;
|
|
19945
|
+
if (!base2) return null;
|
|
19946
|
+
const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
|
|
19947
|
+
timeoutMs: TIMEOUT_MS3,
|
|
19948
|
+
maxBytes: MAX_BYTES2
|
|
19949
|
+
});
|
|
19950
|
+
return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
|
|
19951
|
+
}
|
|
18526
19952
|
var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
|
|
18527
19953
|
async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
18528
19954
|
if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
|
|
@@ -18531,19 +19957,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
18531
19957
|
}
|
|
18532
19958
|
|
|
18533
19959
|
// src/supply-chain/local-resolve.ts
|
|
18534
|
-
var
|
|
18535
|
-
var
|
|
19960
|
+
var import_fs26 = __toESM(require("fs"));
|
|
19961
|
+
var import_path26 = __toESM(require("path"));
|
|
18536
19962
|
var MAX_LEVELS = 32;
|
|
18537
19963
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
18538
19964
|
function resolveInstalled(name, cwd) {
|
|
18539
|
-
if (!cwd || !
|
|
19965
|
+
if (!cwd || !import_path26.default.isAbsolute(cwd)) return null;
|
|
18540
19966
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
18541
|
-
let dir2 =
|
|
19967
|
+
let dir2 = import_path26.default.resolve(cwd);
|
|
18542
19968
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
18543
|
-
const pkgDir =
|
|
18544
|
-
const manifest =
|
|
19969
|
+
const pkgDir = import_path26.default.join(dir2, "node_modules", ...name.split("/"));
|
|
19970
|
+
const manifest = import_path26.default.join(pkgDir, "package.json");
|
|
18545
19971
|
try {
|
|
18546
|
-
const raw =
|
|
19972
|
+
const raw = import_fs26.default.readFileSync(manifest, "utf8");
|
|
18547
19973
|
const parsed = JSON.parse(raw);
|
|
18548
19974
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
18549
19975
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -18551,7 +19977,7 @@ function resolveInstalled(name, cwd) {
|
|
|
18551
19977
|
return null;
|
|
18552
19978
|
} catch {
|
|
18553
19979
|
}
|
|
18554
|
-
const parent =
|
|
19980
|
+
const parent = import_path26.default.dirname(dir2);
|
|
18555
19981
|
if (parent === dir2) break;
|
|
18556
19982
|
dir2 = parent;
|
|
18557
19983
|
}
|
|
@@ -18673,7 +20099,7 @@ function checkInstalled(p, version, misses) {
|
|
|
18673
20099
|
}
|
|
18674
20100
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
18675
20101
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
18676
|
-
const localCwd = cwd &&
|
|
20102
|
+
const localCwd = cwd && import_path27.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path27.default.resolve(dir2, step), cwd) : void 0;
|
|
18677
20103
|
const installed = resolveInstalled(p.name, localCwd);
|
|
18678
20104
|
if (installed && (!p.version || p.version === installed.version))
|
|
18679
20105
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -18716,8 +20142,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
|
18716
20142
|
detail: `${label(p, version)} runs an install script`
|
|
18717
20143
|
});
|
|
18718
20144
|
}
|
|
20145
|
+
if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
|
|
20146
|
+
return byHistory(p, version, info, findings, misses);
|
|
20147
|
+
}
|
|
18719
20148
|
return findings;
|
|
18720
20149
|
}
|
|
20150
|
+
var SOFT = /* @__PURE__ */ new Set(["new", "install-script"]);
|
|
20151
|
+
async function byHistory(p, version, info, findings, misses) {
|
|
20152
|
+
let out = findings;
|
|
20153
|
+
let h = info.history;
|
|
20154
|
+
if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT.has(f.kind))) {
|
|
20155
|
+
try {
|
|
20156
|
+
h = await pypiHistory(p.name, version) ?? void 0;
|
|
20157
|
+
} catch (err) {
|
|
20158
|
+
misses.push(`${label(p, version)}: release history ${err.message}`);
|
|
20159
|
+
}
|
|
20160
|
+
}
|
|
20161
|
+
if (!h || !h.previous) return out;
|
|
20162
|
+
const previous = h.previous;
|
|
20163
|
+
if (h.previousHasInstallScript === true) {
|
|
20164
|
+
out = out.filter((f) => f.kind !== "install-script");
|
|
20165
|
+
}
|
|
20166
|
+
const drop = () => ({
|
|
20167
|
+
pkg: p,
|
|
20168
|
+
version,
|
|
20169
|
+
kind: "provenance-drop",
|
|
20170
|
+
detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
|
|
20171
|
+
});
|
|
20172
|
+
if (p.ecosystem === "npm") {
|
|
20173
|
+
if (h.previousProvenance && h.provenance === false) return [...out, drop()];
|
|
20174
|
+
if (!out.some((f) => SOFT.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
|
|
20175
|
+
const [now, before] = await Promise.all([
|
|
20176
|
+
npmSourceRepo(p.name, version),
|
|
20177
|
+
npmSourceRepo(p.name, previous)
|
|
20178
|
+
]);
|
|
20179
|
+
return vouched(p, version, now, before, out, misses);
|
|
20180
|
+
}
|
|
20181
|
+
if (p.ecosystem === "PyPI") {
|
|
20182
|
+
if (!out.some((f) => SOFT.has(f.kind)) || !info.file || !h.previousFile) return out;
|
|
20183
|
+
const [now, before] = await Promise.all([
|
|
20184
|
+
pypiSourceRepo(p.name, version, info.file),
|
|
20185
|
+
pypiSourceRepo(p.name, previous, h.previousFile)
|
|
20186
|
+
]);
|
|
20187
|
+
if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
|
|
20188
|
+
return vouched(p, version, now, before, out, misses);
|
|
20189
|
+
}
|
|
20190
|
+
return out;
|
|
20191
|
+
}
|
|
20192
|
+
function vouched(p, version, now, before, findings, misses) {
|
|
20193
|
+
if (now === "unknown" || before === "unknown") {
|
|
20194
|
+
misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
|
|
20195
|
+
return findings;
|
|
20196
|
+
}
|
|
20197
|
+
if (now === "none" || before === "none" || now !== before) return findings;
|
|
20198
|
+
return findings.filter((f) => !SOFT.has(f.kind));
|
|
20199
|
+
}
|
|
18721
20200
|
async function runPackageCheck(toolName, args, cfg, cwd) {
|
|
18722
20201
|
if (!cfg.enabled) return ALLOW;
|
|
18723
20202
|
try {
|
|
@@ -19100,7 +20579,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19100
20579
|
fileAccess,
|
|
19101
20580
|
config,
|
|
19102
20581
|
skipIgnoredFastPath,
|
|
19103
|
-
argumentScan: argScan
|
|
20582
|
+
argumentScan: argScan,
|
|
20583
|
+
// A server a person linked to an app is judged as that app (fw#390).
|
|
20584
|
+
mcpServer: resolveLinkedServer(meta?.mcpServer, {
|
|
20585
|
+
serverKey: meta?.serverKey,
|
|
20586
|
+
managedLinks: config.policy.appLinks,
|
|
20587
|
+
appliedShields: config.policy.appliedShields,
|
|
20588
|
+
workspaceGoverned: config.policySource === "workspace"
|
|
20589
|
+
})
|
|
19104
20590
|
});
|
|
19105
20591
|
if (!sessionScript || own.decision === "block") return own;
|
|
19106
20592
|
const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
|
|
@@ -19316,7 +20802,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19316
20802
|
const last = rec.sources[rec.sources.length - 1];
|
|
19317
20803
|
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
19318
20804
|
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
19319
|
-
const
|
|
20805
|
+
const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
19320
20806
|
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
19321
20807
|
const noun = (d) => {
|
|
19322
20808
|
const k = destinationKind(d);
|
|
@@ -19325,14 +20811,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19325
20811
|
const parts = held.map(
|
|
19326
20812
|
(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`
|
|
19327
20813
|
);
|
|
19328
|
-
const home =
|
|
20814
|
+
const home = import_os20.default.homedir();
|
|
19329
20815
|
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
19330
20816
|
for (const w of writeHolds) {
|
|
19331
20817
|
parts.push(
|
|
19332
20818
|
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}`
|
|
19333
20819
|
);
|
|
19334
20820
|
}
|
|
19335
|
-
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${
|
|
20821
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
|
|
19336
20822
|
exposureFromEid = last?.eid;
|
|
19337
20823
|
if (exposureFromEid) exposureMeta = { exposureFromEid };
|
|
19338
20824
|
exposureKeys = [
|