@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.mjs
CHANGED
|
@@ -52,15 +52,101 @@ import picomatch from "picomatch";
|
|
|
52
52
|
import safeRegex from "safe-regex2";
|
|
53
53
|
import pm from "picomatch";
|
|
54
54
|
import mvdanSh from "mvdan-sh";
|
|
55
|
-
import safeRegex2 from "safe-regex2";
|
|
56
55
|
import mvdanSh2 from "mvdan-sh";
|
|
57
|
-
import
|
|
56
|
+
import safeRegex2 from "safe-regex2";
|
|
58
57
|
import { createHash as createHash2 } from "crypto";
|
|
59
58
|
import { createHash as createHash22 } from "crypto";
|
|
60
59
|
import mvdanSh3 from "mvdan-sh";
|
|
61
60
|
import { createHash as createHash3 } from "crypto";
|
|
62
61
|
import mvdanSh4 from "mvdan-sh";
|
|
63
62
|
import crypto from "crypto";
|
|
63
|
+
function validateRegex(pattern) {
|
|
64
|
+
if (!pattern) return "Pattern is required";
|
|
65
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(pattern);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
70
|
+
}
|
|
71
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
72
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
76
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
77
|
+
const key = `${pattern}\0${flags}`;
|
|
78
|
+
if (regexCache.has(key)) {
|
|
79
|
+
const cached = regexCache.get(key);
|
|
80
|
+
regexCache.delete(key);
|
|
81
|
+
regexCache.set(key, cached);
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
if (validateRegex(pattern) !== null) return null;
|
|
85
|
+
try {
|
|
86
|
+
const re = new RegExp(pattern, flags);
|
|
87
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
88
|
+
const oldest = regexCache.keys().next().value;
|
|
89
|
+
if (oldest) regexCache.delete(oldest);
|
|
90
|
+
}
|
|
91
|
+
regexCache.set(key, re);
|
|
92
|
+
return re;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isToolRule(rule) {
|
|
98
|
+
return rule.tool.startsWith("{,mcp__*__}");
|
|
99
|
+
}
|
|
100
|
+
function actionPart(ruleName) {
|
|
101
|
+
const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
|
|
102
|
+
return m ? m[1] : ruleName ?? "rule";
|
|
103
|
+
}
|
|
104
|
+
function ownReason(reason, owner) {
|
|
105
|
+
if (!reason) return reason;
|
|
106
|
+
return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
|
|
107
|
+
}
|
|
108
|
+
function buildAppShield(spec) {
|
|
109
|
+
const owner = spec.label;
|
|
110
|
+
const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
|
|
111
|
+
...rule,
|
|
112
|
+
name: `shield:${spec.name}:${actionPart(rule.name)}`,
|
|
113
|
+
server: spec.server,
|
|
114
|
+
reason: ownReason(rule.reason, owner)
|
|
115
|
+
}));
|
|
116
|
+
const ownTools = (spec.mcpTools ?? []).map((rule) => ({
|
|
117
|
+
...rule,
|
|
118
|
+
server: spec.server
|
|
119
|
+
}));
|
|
120
|
+
return {
|
|
121
|
+
name: spec.name,
|
|
122
|
+
description: spec.description,
|
|
123
|
+
aliases: [],
|
|
124
|
+
smartRules: [...toolRules, ...ownTools, ...spec.doors],
|
|
125
|
+
dangerousWords: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildGeneralRules() {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const name of Object.keys(ACTION_TEMPLATES)) {
|
|
131
|
+
const { slug: slug2, label: label2 } = ACTION_CHECKS[name];
|
|
132
|
+
for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
|
|
133
|
+
const part = actionPart(rule.name).replace(/^review-/, "");
|
|
134
|
+
out.push({
|
|
135
|
+
...rule,
|
|
136
|
+
name: `review-action-${slug2}-${part}`,
|
|
137
|
+
notServer: APP_SERVER_GLOB,
|
|
138
|
+
reason: ownReason(rule.reason, label2)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function generalActionRuleNames(template) {
|
|
145
|
+
const { slug: slug2 } = ACTION_CHECKS[template];
|
|
146
|
+
return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
|
|
147
|
+
(r) => r.name
|
|
148
|
+
);
|
|
149
|
+
}
|
|
64
150
|
function isShieldVerdict(v) {
|
|
65
151
|
return v === "allow" || v === "review" || v === "block";
|
|
66
152
|
}
|
|
@@ -98,16 +184,25 @@ function validateOverrides(raw) {
|
|
|
98
184
|
return { overrides: result, warnings };
|
|
99
185
|
}
|
|
100
186
|
function assertBuiltinShieldRegexesAreSafe() {
|
|
101
|
-
|
|
187
|
+
const sets = [
|
|
188
|
+
...Object.values(BUILTIN_SHIELDS).map((sh) => ({
|
|
189
|
+
name: sh.name,
|
|
190
|
+
smartRules: sh.smartRules
|
|
191
|
+
})),
|
|
192
|
+
...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
|
|
193
|
+
{ name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
|
|
194
|
+
];
|
|
195
|
+
for (const shield of sets) {
|
|
102
196
|
for (const rule of shield.smartRules) {
|
|
103
197
|
const conditions = rule.conditions ?? [];
|
|
104
198
|
for (const cond of conditions) {
|
|
105
199
|
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
106
200
|
const pattern = cond.value;
|
|
107
201
|
if (!pattern) continue;
|
|
108
|
-
|
|
202
|
+
const problem = validateRegex(pattern);
|
|
203
|
+
if (problem) {
|
|
109
204
|
throw new Error(
|
|
110
|
-
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has
|
|
205
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
|
|
111
206
|
);
|
|
112
207
|
}
|
|
113
208
|
}
|
|
@@ -393,7 +488,7 @@ function isLockedCheck(def) {
|
|
|
393
488
|
function ssrfCheckId(tier) {
|
|
394
489
|
return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
|
|
395
490
|
}
|
|
396
|
-
function
|
|
491
|
+
function ruleNameInLabel(s) {
|
|
397
492
|
let rest = s;
|
|
398
493
|
const override = rest.lastIndexOf(OVERRIDE_PREFIX);
|
|
399
494
|
if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
|
|
@@ -409,7 +504,7 @@ function checkIdForRule(nameOrLabel) {
|
|
|
409
504
|
const s = nameOrLabel.trim();
|
|
410
505
|
const exact = LEGACY_TO_ID.get(s);
|
|
411
506
|
if (exact) return exact;
|
|
412
|
-
const unwrapped =
|
|
507
|
+
const unwrapped = ruleNameInLabel(s);
|
|
413
508
|
if (unwrapped !== void 0) return checkIdForRule(unwrapped);
|
|
414
509
|
if (s.startsWith("shield:project-jail:")) {
|
|
415
510
|
return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
|
|
@@ -497,9 +592,9 @@ function resolveCheck(settings, id) {
|
|
|
497
592
|
const on = settings.appliedShields?.includes(def.pack) ?? false;
|
|
498
593
|
return { id, value: on ? def.defaultValue : "off", source: "default" };
|
|
499
594
|
}
|
|
500
|
-
const
|
|
501
|
-
if (
|
|
502
|
-
const value = CHECK_VERDICT_RANK[
|
|
595
|
+
const read2 = LEGACY_READERS[id]?.(settings);
|
|
596
|
+
if (read2 === void 0) return { id, value: def.defaultValue, source: "default" };
|
|
597
|
+
const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
|
|
503
598
|
return { id, value, source: value === def.defaultValue ? "default" : "configured" };
|
|
504
599
|
}
|
|
505
600
|
function checkValue(settings, id) {
|
|
@@ -567,6 +662,102 @@ function scanViews(text) {
|
|
|
567
662
|
}
|
|
568
663
|
return views;
|
|
569
664
|
}
|
|
665
|
+
function isAgentInstructionPath(absPath, root) {
|
|
666
|
+
const r = root.replace(/\/$/, "");
|
|
667
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
668
|
+
const rel = absPath.slice(r.length + 1);
|
|
669
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
670
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
671
|
+
}
|
|
672
|
+
function isInside(root, p) {
|
|
673
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
674
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
675
|
+
}
|
|
676
|
+
function isPasswordHashPath(p) {
|
|
677
|
+
return PASSWORD_HASH_FILES.includes(p);
|
|
678
|
+
}
|
|
679
|
+
function isLegacySystemPath(p) {
|
|
680
|
+
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
681
|
+
}
|
|
682
|
+
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
683
|
+
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
684
|
+
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
685
|
+
const rooted = p.startsWith("/");
|
|
686
|
+
const parts = [];
|
|
687
|
+
for (const part of p.split("/")) {
|
|
688
|
+
if (!part || part === ".") continue;
|
|
689
|
+
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
690
|
+
else if (part !== ".." || !rooted) parts.push(part);
|
|
691
|
+
}
|
|
692
|
+
return (rooted ? "/" : "") + parts.join("/");
|
|
693
|
+
}
|
|
694
|
+
function inlineLanguageOf(interp) {
|
|
695
|
+
const i = interp.toLowerCase();
|
|
696
|
+
if (/^python[\d.]*$/.test(i)) return "python";
|
|
697
|
+
if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
|
|
698
|
+
if (i === "perl") return "perl";
|
|
699
|
+
if (i === "ruby" || i === "irb") return "ruby";
|
|
700
|
+
if (i === "php") return "php";
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
function resolvePythonAliases(code) {
|
|
704
|
+
const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
|
|
705
|
+
let out = code;
|
|
706
|
+
for (const [, mod, alias] of aliases) {
|
|
707
|
+
if (alias === mod) continue;
|
|
708
|
+
out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
function protectedLiteral(code, isProtected) {
|
|
713
|
+
let n = 0;
|
|
714
|
+
for (const m of code.matchAll(STRING_LITERAL)) {
|
|
715
|
+
if (++n > 2048) break;
|
|
716
|
+
const value = m[2].trim();
|
|
717
|
+
if (value && isProtected(value)) return value;
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
function amplifies(language, code) {
|
|
722
|
+
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*\)/;
|
|
723
|
+
const m = loop.exec(code);
|
|
724
|
+
if (!m) return null;
|
|
725
|
+
const body = code.slice(m.index);
|
|
726
|
+
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(
|
|
727
|
+
body
|
|
728
|
+
) ? m[0] : null;
|
|
729
|
+
}
|
|
730
|
+
function judgeInlineCode(language, code, isProtected) {
|
|
731
|
+
if (code.length > MAX_INLINE_CODE) {
|
|
732
|
+
return { outcome: "unreadable", what: "is too long for node9 to read" };
|
|
733
|
+
}
|
|
734
|
+
const text = language === "python" ? resolvePythonAliases(code) : code;
|
|
735
|
+
for (const [category, re] of TABLES[language]) {
|
|
736
|
+
const m = re.exec(text);
|
|
737
|
+
if (!m) continue;
|
|
738
|
+
const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
|
|
739
|
+
return {
|
|
740
|
+
outcome: category === "builds-code" ? "unreadable" : "dangerous",
|
|
741
|
+
what: WHAT[category],
|
|
742
|
+
evidence,
|
|
743
|
+
category
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const path28 = protectedLiteral(text, isProtected);
|
|
747
|
+
if (path28) {
|
|
748
|
+
return {
|
|
749
|
+
outcome: "dangerous",
|
|
750
|
+
what: WHAT["reads-protected"],
|
|
751
|
+
evidence: path28.slice(0, 60),
|
|
752
|
+
category: "reads-protected"
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const loop = amplifies(language, text);
|
|
756
|
+
if (loop) {
|
|
757
|
+
return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
570
761
|
function isCatHeredocOrLit(part) {
|
|
571
762
|
if (!part) return false;
|
|
572
763
|
const t = syntax.NodeType(part);
|
|
@@ -635,6 +826,55 @@ function cachedNormalize(command, compute) {
|
|
|
635
826
|
function normalizeCommandForPolicy(command) {
|
|
636
827
|
return commandReadingsImpl(command).posix;
|
|
637
828
|
}
|
|
829
|
+
function fileAccessView(command) {
|
|
830
|
+
const hit = accessViewCache.get(command);
|
|
831
|
+
if (hit !== void 0) return hit;
|
|
832
|
+
const view = fileAccessViewImpl(command);
|
|
833
|
+
if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
|
|
834
|
+
const oldest = accessViewCache.keys().next().value;
|
|
835
|
+
if (oldest !== void 0) accessViewCache.delete(oldest);
|
|
836
|
+
}
|
|
837
|
+
accessViewCache.set(command, view);
|
|
838
|
+
return view;
|
|
839
|
+
}
|
|
840
|
+
function fileAccessViewImpl(command) {
|
|
841
|
+
const text = command.replace(INTERPRETER_LINE, "");
|
|
842
|
+
const f = parseShared(text);
|
|
843
|
+
if (f === PARSE_FAIL) return text;
|
|
844
|
+
const toCharIndex = byteOffsetToCharIndex(text);
|
|
845
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
846
|
+
const edits = [];
|
|
847
|
+
try {
|
|
848
|
+
syntax.Walk(f, (node) => {
|
|
849
|
+
if (!node) return false;
|
|
850
|
+
const n = node;
|
|
851
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
852
|
+
const args = n.Args || [];
|
|
853
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
854
|
+
const head = unwrapCommandHead(words);
|
|
855
|
+
for (const i of /* @__PURE__ */ new Set([0, head])) {
|
|
856
|
+
const word = words[i];
|
|
857
|
+
if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (!PROGRAM_DIR.test(word)) continue;
|
|
861
|
+
const name = word.split(/[\\/]/).pop() ?? "";
|
|
862
|
+
if (!name) continue;
|
|
863
|
+
const s = at(args[i].Pos().Offset());
|
|
864
|
+
const e = at(args[i].End().Offset());
|
|
865
|
+
if (s < 0 || e < 0) continue;
|
|
866
|
+
edits.push([s, e, name]);
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
});
|
|
870
|
+
} catch {
|
|
871
|
+
return text;
|
|
872
|
+
}
|
|
873
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
874
|
+
let out = text;
|
|
875
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
876
|
+
return out;
|
|
877
|
+
}
|
|
638
878
|
function commandReadings(command) {
|
|
639
879
|
const r = commandReadingsImpl(command);
|
|
640
880
|
const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
|
|
@@ -1090,53 +1330,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
|
|
|
1090
1330
|
}
|
|
1091
1331
|
return i;
|
|
1092
1332
|
}
|
|
1093
|
-
function
|
|
1333
|
+
function heredocText(word) {
|
|
1334
|
+
const parts = word?.Parts ?? [];
|
|
1335
|
+
let out = "";
|
|
1336
|
+
for (const p of parts) {
|
|
1337
|
+
if (syntax.NodeType(p) !== "Lit") return null;
|
|
1338
|
+
out += p.Value ?? "";
|
|
1339
|
+
}
|
|
1340
|
+
return out;
|
|
1341
|
+
}
|
|
1342
|
+
function stdinCodeOf(redirs) {
|
|
1343
|
+
const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
|
|
1344
|
+
const herestring = deriveRedirOp("cat <<< x");
|
|
1345
|
+
for (const r of redirs) {
|
|
1346
|
+
if (!r || !redirStdinOps().has(r.Op)) continue;
|
|
1347
|
+
if (heredoc.has(r.Op)) {
|
|
1348
|
+
const text = heredocText(r.Hdoc);
|
|
1349
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1350
|
+
}
|
|
1351
|
+
if (r.Op === herestring) {
|
|
1352
|
+
const text = resolveWordLiteral(r.Word);
|
|
1353
|
+
return text === null ? { code: null, source: "variable" } : { code: text };
|
|
1354
|
+
}
|
|
1355
|
+
return { code: null, source: "file" };
|
|
1356
|
+
}
|
|
1357
|
+
return { code: null, source: "file" };
|
|
1358
|
+
}
|
|
1359
|
+
function attachedCode(interp, flag) {
|
|
1360
|
+
const eq = flag.indexOf("=");
|
|
1361
|
+
if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
|
|
1362
|
+
if (/^(pwsh|powershell)/.test(interp)) return null;
|
|
1363
|
+
const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1364
|
+
const body = flag.slice(1);
|
|
1365
|
+
const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
|
|
1366
|
+
if (at < 0) return null;
|
|
1367
|
+
let rest = body.slice(at + 1);
|
|
1368
|
+
if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
|
|
1369
|
+
return rest === "" ? null : rest;
|
|
1370
|
+
}
|
|
1371
|
+
function inlineExecCall(stmt, pipeFed) {
|
|
1094
1372
|
const cmd = stmt?.Cmd;
|
|
1095
|
-
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return
|
|
1373
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
|
|
1096
1374
|
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1097
|
-
if (words.length === 0) return
|
|
1375
|
+
if (words.length === 0) return null;
|
|
1098
1376
|
const headIdx = unwrapCommandHead(words);
|
|
1099
1377
|
const rawHead = words[headIdx];
|
|
1100
|
-
if (rawHead == null) return
|
|
1378
|
+
if (rawHead == null) return null;
|
|
1101
1379
|
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1102
|
-
if (!INLINE_INTERPRETER.test(interp)) return
|
|
1380
|
+
if (!INLINE_INTERPRETER.test(interp)) return null;
|
|
1103
1381
|
let args = words.slice(headIdx + 1);
|
|
1104
|
-
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval")
|
|
1382
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
|
|
1383
|
+
const code = args[1] ?? null;
|
|
1384
|
+
return { interp, code: [code], source: code === null ? "variable" : void 0 };
|
|
1385
|
+
}
|
|
1105
1386
|
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1106
1387
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1107
1388
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1108
1389
|
}
|
|
1390
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1391
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1392
|
+
const fromStdin = () => {
|
|
1393
|
+
if (stdinFed) {
|
|
1394
|
+
const { code, source } = stdinCodeOf(redirs);
|
|
1395
|
+
return { interp, code: [code], source };
|
|
1396
|
+
}
|
|
1397
|
+
return { interp, code: [null], source: "pipe" };
|
|
1398
|
+
};
|
|
1109
1399
|
let positionals2 = 0;
|
|
1110
1400
|
let selectedProgram = false;
|
|
1401
|
+
const codes = [];
|
|
1111
1402
|
for (let i = 0; i < args.length; i++) {
|
|
1112
1403
|
const a = args[i];
|
|
1113
1404
|
if (a == null) {
|
|
1405
|
+
if (codes.length > 0) break;
|
|
1114
1406
|
positionals2++;
|
|
1115
1407
|
selectedProgram = true;
|
|
1116
1408
|
continue;
|
|
1117
1409
|
}
|
|
1118
1410
|
if (!selectedProgram && isInlineCodeFlag(interp, a)) {
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1411
|
+
if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
|
|
1412
|
+
const payload = args[i + 1];
|
|
1413
|
+
if (payload == null) return { interp, code: [null], source: "variable" };
|
|
1414
|
+
return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
|
|
1415
|
+
}
|
|
1416
|
+
const attached = attachedCode(interp, a);
|
|
1417
|
+
if (attached !== null) {
|
|
1418
|
+
codes.push(attached);
|
|
1419
|
+
} else {
|
|
1420
|
+
codes.push(args[i + 1] ?? null);
|
|
1421
|
+
i++;
|
|
1422
|
+
}
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (codes.length > 0) {
|
|
1426
|
+
if (a.startsWith("-")) continue;
|
|
1427
|
+
break;
|
|
1123
1428
|
}
|
|
1124
1429
|
if (a === "-m") {
|
|
1125
1430
|
selectedProgram = true;
|
|
1126
1431
|
continue;
|
|
1127
1432
|
}
|
|
1128
|
-
if (a === "-" && !selectedProgram) return
|
|
1433
|
+
if (a === "-" && !selectedProgram) return fromStdin();
|
|
1129
1434
|
if (!a.startsWith("-")) {
|
|
1130
1435
|
positionals2++;
|
|
1131
1436
|
selectedProgram = true;
|
|
1132
1437
|
}
|
|
1133
1438
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1439
|
+
if (codes.length > 0) {
|
|
1440
|
+
return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
|
|
1441
|
+
}
|
|
1136
1442
|
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
1137
|
-
if (!/^(bash|sh|zsh)$/i.test(interp)) return
|
|
1443
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
|
|
1138
1444
|
}
|
|
1139
|
-
return
|
|
1445
|
+
return null;
|
|
1140
1446
|
}
|
|
1141
1447
|
function shellPayloadNeedsReview(payload) {
|
|
1142
1448
|
if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
|
|
@@ -1169,7 +1475,7 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1169
1475
|
payload
|
|
1170
1476
|
))
|
|
1171
1477
|
review = true;
|
|
1172
|
-
if (!review &&
|
|
1478
|
+
if (!review && inlineExecReview(payload) !== null) review = true;
|
|
1173
1479
|
} catch {
|
|
1174
1480
|
review = true;
|
|
1175
1481
|
} finally {
|
|
@@ -1177,35 +1483,77 @@ function shellPayloadNeedsReview(payload) {
|
|
|
1177
1483
|
}
|
|
1178
1484
|
return review;
|
|
1179
1485
|
}
|
|
1180
|
-
function
|
|
1486
|
+
function inlineExecCalls(command) {
|
|
1181
1487
|
const f = parseShared(command);
|
|
1182
|
-
if (f === PARSE_FAIL)
|
|
1183
|
-
|
|
1184
|
-
command
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
let found = false;
|
|
1488
|
+
if (f === PARSE_FAIL) return PARSE_FAIL;
|
|
1489
|
+
const calls = [];
|
|
1188
1490
|
try {
|
|
1189
1491
|
syntax.Walk(f, (node) => {
|
|
1190
|
-
if (!node ||
|
|
1492
|
+
if (!node || calls.length >= MAX_INLINE_CALLS) return false;
|
|
1191
1493
|
const n = node;
|
|
1192
1494
|
const t = syntax.NodeType(n);
|
|
1193
1495
|
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
return false;
|
|
1197
|
-
}
|
|
1496
|
+
const call = inlineExecCall(n.Y, true);
|
|
1497
|
+
if (call) calls.push(call);
|
|
1198
1498
|
}
|
|
1199
|
-
if (t === "Stmt"
|
|
1200
|
-
|
|
1201
|
-
|
|
1499
|
+
if (t === "Stmt") {
|
|
1500
|
+
const call = inlineExecCall(n, false);
|
|
1501
|
+
if (call) calls.push(call);
|
|
1202
1502
|
}
|
|
1203
1503
|
return true;
|
|
1204
1504
|
});
|
|
1205
1505
|
} catch {
|
|
1206
|
-
return found;
|
|
1207
1506
|
}
|
|
1208
|
-
return
|
|
1507
|
+
return calls;
|
|
1508
|
+
}
|
|
1509
|
+
function detectInlineExec(command) {
|
|
1510
|
+
const calls = inlineExecCalls(command);
|
|
1511
|
+
if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
|
|
1512
|
+
return calls.length > 0;
|
|
1513
|
+
}
|
|
1514
|
+
function isProtectedCodePath(p) {
|
|
1515
|
+
if (matchSensitivePath(p) !== null) return true;
|
|
1516
|
+
return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
|
|
1517
|
+
}
|
|
1518
|
+
function inlineExecDescription(r) {
|
|
1519
|
+
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.";
|
|
1520
|
+
return `${lead} Review the full script below before allowing it to execute.`;
|
|
1521
|
+
}
|
|
1522
|
+
function inlineExecReason(r) {
|
|
1523
|
+
const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
|
|
1524
|
+
return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
|
|
1525
|
+
}
|
|
1526
|
+
function inlineExecReview(command) {
|
|
1527
|
+
const calls = inlineExecCalls(command);
|
|
1528
|
+
if (calls === PARSE_FAIL) {
|
|
1529
|
+
return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
|
|
1530
|
+
}
|
|
1531
|
+
let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
|
|
1532
|
+
for (const call of calls) {
|
|
1533
|
+
const language = inlineLanguageOf(call.interp);
|
|
1534
|
+
for (const code of call.code) {
|
|
1535
|
+
let finding = null;
|
|
1536
|
+
if (code === null) {
|
|
1537
|
+
finding = {
|
|
1538
|
+
outcome: "unreadable",
|
|
1539
|
+
interp: call.interp,
|
|
1540
|
+
what: UNREADABLE_SOURCE[call.source ?? "variable"]
|
|
1541
|
+
};
|
|
1542
|
+
} else if (language === null) {
|
|
1543
|
+
finding = {
|
|
1544
|
+
outcome: "unreadable",
|
|
1545
|
+
interp: call.interp,
|
|
1546
|
+
what: `is ${call.interp} code, which node9 does not read`
|
|
1547
|
+
};
|
|
1548
|
+
} else {
|
|
1549
|
+
const judged = judgeInlineCode(language, code, isProtectedCodePath);
|
|
1550
|
+
if (judged) finding = { ...judged, interp: call.interp };
|
|
1551
|
+
}
|
|
1552
|
+
if (finding?.outcome === "dangerous") return finding;
|
|
1553
|
+
if (finding && !unreadable) unreadable = finding;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return unreadable;
|
|
1209
1557
|
}
|
|
1210
1558
|
function analyzeChmod777(command) {
|
|
1211
1559
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
@@ -2454,40 +2802,6 @@ function analyzeShellCommand(command) {
|
|
|
2454
2802
|
}
|
|
2455
2803
|
return { actions, paths, allTokens };
|
|
2456
2804
|
}
|
|
2457
|
-
function validateRegex(pattern) {
|
|
2458
|
-
if (!pattern) return "Pattern is required";
|
|
2459
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2460
|
-
try {
|
|
2461
|
-
new RegExp(pattern);
|
|
2462
|
-
} catch (e) {
|
|
2463
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2464
|
-
}
|
|
2465
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2466
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2470
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2471
|
-
const key = `${pattern}\0${flags}`;
|
|
2472
|
-
if (regexCache.has(key)) {
|
|
2473
|
-
const cached = regexCache.get(key);
|
|
2474
|
-
regexCache.delete(key);
|
|
2475
|
-
regexCache.set(key, cached);
|
|
2476
|
-
return cached;
|
|
2477
|
-
}
|
|
2478
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2479
|
-
try {
|
|
2480
|
-
const re = new RegExp(pattern, flags);
|
|
2481
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2482
|
-
const oldest = regexCache.keys().next().value;
|
|
2483
|
-
if (oldest) regexCache.delete(oldest);
|
|
2484
|
-
}
|
|
2485
|
-
regexCache.set(key, re);
|
|
2486
|
-
return re;
|
|
2487
|
-
} catch {
|
|
2488
|
-
return null;
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
2805
|
function matchesPattern(text, patterns) {
|
|
2492
2806
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2493
2807
|
if (p.length === 0) return false;
|
|
@@ -2498,9 +2812,21 @@ function matchesPattern(text, patterns) {
|
|
|
2498
2812
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2499
2813
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2500
2814
|
}
|
|
2501
|
-
function
|
|
2815
|
+
function mcpServerFromToolName(toolName) {
|
|
2816
|
+
const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
2817
|
+
return match?.[1];
|
|
2818
|
+
}
|
|
2819
|
+
function ruleServerMatches(rule, serverLabel) {
|
|
2820
|
+
const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
|
|
2821
|
+
if (rule.server) {
|
|
2822
|
+
if (!label2 || !matchesPattern(label2, rule.server)) return false;
|
|
2823
|
+
}
|
|
2824
|
+
if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
|
|
2825
|
+
return true;
|
|
2826
|
+
}
|
|
2827
|
+
function getNestedValue(obj, path28) {
|
|
2502
2828
|
if (!obj || typeof obj !== "object") return null;
|
|
2503
|
-
const segments2 =
|
|
2829
|
+
const segments2 = path28.split(".");
|
|
2504
2830
|
for (const seg of segments2) {
|
|
2505
2831
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2506
2832
|
}
|
|
@@ -2520,23 +2846,26 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2520
2846
|
return val;
|
|
2521
2847
|
};
|
|
2522
2848
|
const readingsCache = /* @__PURE__ */ new Map();
|
|
2523
|
-
const resolveFieldReadings = (field) => {
|
|
2524
|
-
const
|
|
2849
|
+
const resolveFieldReadings = (field, reading) => {
|
|
2850
|
+
const access = reading === "access" && field === "command";
|
|
2851
|
+
const key = access ? `${field}\0access` : field;
|
|
2852
|
+
const cached = readingsCache.get(key);
|
|
2525
2853
|
if (cached) return cached;
|
|
2526
2854
|
const primary = resolveField(field);
|
|
2527
2855
|
if (primary === null) {
|
|
2528
|
-
readingsCache.set(
|
|
2856
|
+
readingsCache.set(key, []);
|
|
2529
2857
|
return [];
|
|
2530
2858
|
}
|
|
2531
2859
|
let out = [primary];
|
|
2532
2860
|
if (field === "command") {
|
|
2533
2861
|
const raw = getNestedValue(args, field);
|
|
2534
2862
|
if (typeof raw === "string") {
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2863
|
+
const text = access ? fileAccessView(raw) : raw;
|
|
2864
|
+
const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
|
|
2865
|
+
out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
2537
2866
|
}
|
|
2538
2867
|
}
|
|
2539
|
-
readingsCache.set(
|
|
2868
|
+
readingsCache.set(key, out);
|
|
2540
2869
|
return out;
|
|
2541
2870
|
};
|
|
2542
2871
|
const results = rule.conditions.map((cond) => {
|
|
@@ -2554,14 +2883,14 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2554
2883
|
if (val === null || !cond.value) return false;
|
|
2555
2884
|
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2556
2885
|
if (!reM) return false;
|
|
2557
|
-
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
2886
|
+
return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
|
|
2558
2887
|
}
|
|
2559
2888
|
case "notMatches": {
|
|
2560
2889
|
if (!cond.value) return false;
|
|
2561
2890
|
if (val === null) return true;
|
|
2562
2891
|
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2563
2892
|
if (!reN) return false;
|
|
2564
|
-
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
2893
|
+
return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
|
|
2565
2894
|
}
|
|
2566
2895
|
case "matchesGlob":
|
|
2567
2896
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
@@ -3403,35 +3732,6 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
3403
3732
|
}
|
|
3404
3733
|
return out;
|
|
3405
3734
|
}
|
|
3406
|
-
function isAgentInstructionPath(absPath, root) {
|
|
3407
|
-
const r = root.replace(/\/$/, "");
|
|
3408
|
-
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
3409
|
-
const rel = absPath.slice(r.length + 1);
|
|
3410
|
-
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
3411
|
-
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
3412
|
-
}
|
|
3413
|
-
function isInside(root, p) {
|
|
3414
|
-
const r = root.replace(/\/$/, "") || "/";
|
|
3415
|
-
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
3416
|
-
}
|
|
3417
|
-
function isPasswordHashPath(p) {
|
|
3418
|
-
return PASSWORD_HASH_FILES.includes(p);
|
|
3419
|
-
}
|
|
3420
|
-
function isLegacySystemPath(p) {
|
|
3421
|
-
return LEGACY_SYSTEM_PATH_RE.test(p);
|
|
3422
|
-
}
|
|
3423
|
-
function normalizeFilePath(p, cwd = "", home = "~") {
|
|
3424
|
-
if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
|
|
3425
|
-
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
3426
|
-
const rooted = p.startsWith("/");
|
|
3427
|
-
const parts = [];
|
|
3428
|
-
for (const part of p.split("/")) {
|
|
3429
|
-
if (!part || part === ".") continue;
|
|
3430
|
-
if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
3431
|
-
else if (part !== ".." || !rooted) parts.push(part);
|
|
3432
|
-
}
|
|
3433
|
-
return (rooted ? "/" : "") + parts.join("/");
|
|
3434
|
-
}
|
|
3435
3735
|
function incompleteFileVerdict(operation = "write") {
|
|
3436
3736
|
return {
|
|
3437
3737
|
decision: "review",
|
|
@@ -3483,8 +3783,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
3483
3783
|
}
|
|
3484
3784
|
return out;
|
|
3485
3785
|
}
|
|
3486
|
-
function within(
|
|
3487
|
-
return
|
|
3786
|
+
function within(path28, root) {
|
|
3787
|
+
return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
|
|
3488
3788
|
}
|
|
3489
3789
|
function hits(access, paths, directories, context) {
|
|
3490
3790
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -4292,14 +4592,14 @@ function sensitivePathMatch(originalPath) {
|
|
|
4292
4592
|
}
|
|
4293
4593
|
function assertBuiltinPatternsAreSafe() {
|
|
4294
4594
|
for (const p of DLP_PATTERNS) {
|
|
4295
|
-
if (!
|
|
4595
|
+
if (!safeRegex2(p.regex.source)) {
|
|
4296
4596
|
throw new Error(
|
|
4297
4597
|
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
4298
4598
|
);
|
|
4299
4599
|
}
|
|
4300
4600
|
}
|
|
4301
4601
|
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
4302
|
-
if (!
|
|
4602
|
+
if (!safeRegex2(re.source)) {
|
|
4303
4603
|
throw new Error(
|
|
4304
4604
|
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
4305
4605
|
);
|
|
@@ -5423,9 +5723,9 @@ function bareToolName(toolName) {
|
|
|
5423
5723
|
const parts = toolName.split("__");
|
|
5424
5724
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
5425
5725
|
}
|
|
5426
|
-
function valuesAt(args,
|
|
5726
|
+
function valuesAt(args, path28) {
|
|
5427
5727
|
let cursors = [args];
|
|
5428
|
-
for (const rawSegment of
|
|
5728
|
+
for (const rawSegment of path28.split(".")) {
|
|
5429
5729
|
const isArray = rawSegment.endsWith("[]");
|
|
5430
5730
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
5431
5731
|
const next = [];
|
|
@@ -5456,8 +5756,8 @@ function extractToolDestinations(toolName, args) {
|
|
|
5456
5756
|
try {
|
|
5457
5757
|
const out = [];
|
|
5458
5758
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
|
|
5459
|
-
for (const
|
|
5460
|
-
for (const value of valuesAt(args,
|
|
5759
|
+
for (const path28 of paths) {
|
|
5760
|
+
for (const value of valuesAt(args, path28)) {
|
|
5461
5761
|
const host2 = hostOf(value);
|
|
5462
5762
|
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
5463
5763
|
}
|
|
@@ -5483,8 +5783,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5483
5783
|
try {
|
|
5484
5784
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
5485
5785
|
if (!paths) return null;
|
|
5486
|
-
for (const
|
|
5487
|
-
for (const value of valuesAt(args,
|
|
5786
|
+
for (const path28 of paths) {
|
|
5787
|
+
for (const value of valuesAt(args, path28)) {
|
|
5488
5788
|
const host2 = hostOf(value);
|
|
5489
5789
|
if (!host2) continue;
|
|
5490
5790
|
const m = classifySsrf(host2);
|
|
@@ -5493,7 +5793,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
5493
5793
|
const decision = ssrfFloorDecision(m, written, opts);
|
|
5494
5794
|
if (decision === null) continue;
|
|
5495
5795
|
const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
|
|
5496
|
-
return { ...m, argPath:
|
|
5796
|
+
return { ...m, argPath: path28, host: host2, reason, decision };
|
|
5497
5797
|
}
|
|
5498
5798
|
}
|
|
5499
5799
|
return null;
|
|
@@ -5873,11 +6173,15 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5873
6173
|
};
|
|
5874
6174
|
const ruleCheck = (rule) => {
|
|
5875
6175
|
if (rule.verdict === "allow") return void 0;
|
|
5876
|
-
if (!rule.builtin
|
|
6176
|
+
if (!rule.builtin) return void 0;
|
|
5877
6177
|
const id = checkIdForRule(rule.name);
|
|
5878
6178
|
if (!id) return void 0;
|
|
5879
6179
|
const def = getCheck(id);
|
|
5880
6180
|
if (!def) return void 0;
|
|
6181
|
+
if (rule.pinned) {
|
|
6182
|
+
const explicit = config.policy.checks?.[id];
|
|
6183
|
+
return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
|
|
6184
|
+
}
|
|
5881
6185
|
if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
|
|
5882
6186
|
if (def.pack) {
|
|
5883
6187
|
const explicit = config.policy.checks?.[id];
|
|
@@ -5885,8 +6189,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5885
6189
|
}
|
|
5886
6190
|
return { id, value: cv(id) };
|
|
5887
6191
|
};
|
|
6192
|
+
const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
|
|
5888
6193
|
const matches = config.policy.smartRules.filter((rule) => {
|
|
5889
6194
|
if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
|
|
6195
|
+
if (!ruleServerMatches(rule, serverLabel)) return false;
|
|
5890
6196
|
if (astSuppressed(rule)) return false;
|
|
5891
6197
|
if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
|
|
5892
6198
|
if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
|
|
@@ -5963,11 +6269,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5963
6269
|
if (v?.logged) noteLogged(v);
|
|
5964
6270
|
else if (v) candidates.push(v);
|
|
5965
6271
|
};
|
|
5966
|
-
|
|
6272
|
+
const inline = config.settings.mode === "strict" ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
|
|
6273
|
+
if (inline !== null) {
|
|
5967
6274
|
consider("commands.inline-exec", {
|
|
5968
6275
|
decision: "review",
|
|
5969
6276
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
5970
|
-
|
|
6277
|
+
...inline ? { reason: inlineExecReason(inline) } : {},
|
|
6278
|
+
ruleDescription: inlineExecDescription(inline),
|
|
5971
6279
|
tier: 3,
|
|
5972
6280
|
checkId: "commands.inline-exec"
|
|
5973
6281
|
});
|
|
@@ -7057,10 +7365,13 @@ function matchCanaryArgs(args, values) {
|
|
|
7057
7365
|
return null;
|
|
7058
7366
|
}
|
|
7059
7367
|
}
|
|
7060
|
-
var aws_default, bash_safe_default, docker_default, filesystem_default,
|
|
7368
|
+
var MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, finance_default, identity_default, publishing_default, ACTION_TEMPLATES, ACTION_CHECKS, GAP, prog, httpTo, httpWrite, HTTP_WRITE, HTTP_WRITE_ANY, NOT_STRIPE_TEST, postTo, STRIPE_OBJECTS, APP_SHIELD_SPECS, APP_SHIELDS, APP_SHIELD_NAMES, APP_SERVER_GLOB, GENERAL_ACTION_RULES, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, WHAT, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, TABLES, STRING_LITERAL, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, INTERPRETER_LINE, PROGRAM_DIR, ACCESS_VIEW_CACHE_MAX, accessViewCache, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, MAX_INLINE_CALLS, INLINE_EXEC_FALLBACK, UNREADABLE_SOURCE, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
7061
7369
|
var init_dist = __esm({
|
|
7062
7370
|
"packages/policy-engine/dist/index.mjs"() {
|
|
7063
7371
|
"use strict";
|
|
7372
|
+
MAX_REGEX_LENGTH = 256;
|
|
7373
|
+
REGEX_CACHE_MAX = 500;
|
|
7374
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
7064
7375
|
aws_default = {
|
|
7065
7376
|
name: "aws",
|
|
7066
7377
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -7362,81 +7673,10 @@ var init_dist = __esm({
|
|
|
7362
7673
|
],
|
|
7363
7674
|
dangerousWords: ["wipefs"]
|
|
7364
7675
|
};
|
|
7365
|
-
|
|
7366
|
-
name: "
|
|
7367
|
-
description: "
|
|
7368
|
-
aliases: ["
|
|
7369
|
-
smartRules: [
|
|
7370
|
-
{
|
|
7371
|
-
name: "shield:finance:review-refund",
|
|
7372
|
-
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
7373
|
-
conditions: [],
|
|
7374
|
-
verdict: "review",
|
|
7375
|
-
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
7376
|
-
},
|
|
7377
|
-
{
|
|
7378
|
-
name: "shield:finance:review-payout",
|
|
7379
|
-
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
7380
|
-
conditions: [],
|
|
7381
|
-
verdict: "review",
|
|
7382
|
-
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
7383
|
-
},
|
|
7384
|
-
{
|
|
7385
|
-
name: "shield:finance:review-transfer",
|
|
7386
|
-
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction,create_transaction}",
|
|
7387
|
-
conditions: [],
|
|
7388
|
-
verdict: "review",
|
|
7389
|
-
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
7390
|
-
},
|
|
7391
|
-
{
|
|
7392
|
-
name: "shield:finance:review-charge",
|
|
7393
|
-
tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
|
|
7394
|
-
conditions: [],
|
|
7395
|
-
verdict: "review",
|
|
7396
|
-
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
7397
|
-
},
|
|
7398
|
-
{
|
|
7399
|
-
name: "shield:finance:review-payment-link",
|
|
7400
|
-
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
7401
|
-
conditions: [],
|
|
7402
|
-
verdict: "review",
|
|
7403
|
-
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
7404
|
-
},
|
|
7405
|
-
{
|
|
7406
|
-
name: "shield:finance:review-subscription",
|
|
7407
|
-
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
7408
|
-
conditions: [],
|
|
7409
|
-
verdict: "review",
|
|
7410
|
-
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
7411
|
-
},
|
|
7412
|
-
{
|
|
7413
|
-
name: "shield:finance:review-dispute",
|
|
7414
|
-
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
7415
|
-
conditions: [],
|
|
7416
|
-
verdict: "review",
|
|
7417
|
-
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
7418
|
-
},
|
|
7419
|
-
{
|
|
7420
|
-
name: "shield:finance:review-card",
|
|
7421
|
-
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
7422
|
-
conditions: [],
|
|
7423
|
-
verdict: "review",
|
|
7424
|
-
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
7425
|
-
},
|
|
7426
|
-
{
|
|
7427
|
-
name: "shield:finance:review-withdraw",
|
|
7428
|
-
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
7429
|
-
conditions: [],
|
|
7430
|
-
verdict: "review",
|
|
7431
|
-
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
7432
|
-
}
|
|
7433
|
-
],
|
|
7434
|
-
dangerousWords: []
|
|
7435
|
-
};
|
|
7436
|
-
github_default = {
|
|
7437
|
-
name: "github",
|
|
7438
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
7439
|
-
aliases: ["git"],
|
|
7676
|
+
github_default = {
|
|
7677
|
+
name: "github",
|
|
7678
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
7679
|
+
aliases: ["git"],
|
|
7440
7680
|
smartRules: [
|
|
7441
7681
|
{
|
|
7442
7682
|
name: "shield:github:review-delete-branch-remote",
|
|
@@ -7459,56 +7699,6 @@ var init_dist = __esm({
|
|
|
7459
7699
|
],
|
|
7460
7700
|
dangerousWords: []
|
|
7461
7701
|
};
|
|
7462
|
-
identity_default = {
|
|
7463
|
-
name: "identity",
|
|
7464
|
-
description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
|
|
7465
|
-
aliases: ["access", "iam"],
|
|
7466
|
-
smartRules: [
|
|
7467
|
-
{
|
|
7468
|
-
name: "shield:identity:review-password",
|
|
7469
|
-
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
7470
|
-
conditions: [],
|
|
7471
|
-
verdict: "review",
|
|
7472
|
-
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
7473
|
-
},
|
|
7474
|
-
{
|
|
7475
|
-
name: "shield:identity:review-key",
|
|
7476
|
-
tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
|
|
7477
|
-
conditions: [],
|
|
7478
|
-
verdict: "review",
|
|
7479
|
-
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
7480
|
-
},
|
|
7481
|
-
{
|
|
7482
|
-
name: "shield:identity:review-access",
|
|
7483
|
-
tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy}",
|
|
7484
|
-
conditions: [],
|
|
7485
|
-
verdict: "review",
|
|
7486
|
-
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
7487
|
-
},
|
|
7488
|
-
{
|
|
7489
|
-
name: "shield:identity:review-membership",
|
|
7490
|
-
tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member}",
|
|
7491
|
-
conditions: [],
|
|
7492
|
-
verdict: "review",
|
|
7493
|
-
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
7494
|
-
},
|
|
7495
|
-
{
|
|
7496
|
-
name: "shield:identity:review-user",
|
|
7497
|
-
tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
7498
|
-
conditions: [],
|
|
7499
|
-
verdict: "review",
|
|
7500
|
-
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
7501
|
-
},
|
|
7502
|
-
{
|
|
7503
|
-
name: "shield:identity:review-mfa",
|
|
7504
|
-
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
7505
|
-
conditions: [],
|
|
7506
|
-
verdict: "review",
|
|
7507
|
-
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
7508
|
-
}
|
|
7509
|
-
],
|
|
7510
|
-
dangerousWords: []
|
|
7511
|
-
};
|
|
7512
7702
|
k8s_default = {
|
|
7513
7703
|
name: "k8s",
|
|
7514
7704
|
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
@@ -7761,7 +7951,7 @@ var init_dist = __esm({
|
|
|
7761
7951
|
{
|
|
7762
7952
|
field: "command",
|
|
7763
7953
|
op: "matches",
|
|
7764
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
7954
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development)){0,2}(?=[\\s;&|>)<]|$)",
|
|
7765
7955
|
flags: "i"
|
|
7766
7956
|
}
|
|
7767
7957
|
],
|
|
@@ -7775,7 +7965,7 @@ var init_dist = __esm({
|
|
|
7775
7965
|
{
|
|
7776
7966
|
field: "command",
|
|
7777
7967
|
op: "matches",
|
|
7778
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
7968
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
7779
7969
|
flags: "i"
|
|
7780
7970
|
}
|
|
7781
7971
|
],
|
|
@@ -7825,18 +8015,218 @@ var init_dist = __esm({
|
|
|
7825
8015
|
reason: "Reading .env files is blocked by project-jail shield"
|
|
7826
8016
|
},
|
|
7827
8017
|
{
|
|
7828
|
-
name: "shield:project-jail:review-read-credentials-any-tool",
|
|
7829
|
-
tool: "*",
|
|
7830
|
-
conditions: [
|
|
7831
|
-
{
|
|
7832
|
-
field: "file_path",
|
|
7833
|
-
op: "matches",
|
|
7834
|
-
value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
|
|
7835
|
-
flags: "i"
|
|
7836
|
-
}
|
|
7837
|
-
],
|
|
8018
|
+
name: "shield:project-jail:review-read-credentials-any-tool",
|
|
8019
|
+
tool: "*",
|
|
8020
|
+
conditions: [
|
|
8021
|
+
{
|
|
8022
|
+
field: "file_path",
|
|
8023
|
+
op: "matches",
|
|
8024
|
+
value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
|
|
8025
|
+
flags: "i"
|
|
8026
|
+
}
|
|
8027
|
+
],
|
|
8028
|
+
verdict: "review",
|
|
8029
|
+
reason: "Reading credential files requires approval (project-jail shield)"
|
|
8030
|
+
}
|
|
8031
|
+
],
|
|
8032
|
+
dangerousWords: []
|
|
8033
|
+
};
|
|
8034
|
+
redis_default = {
|
|
8035
|
+
name: "redis",
|
|
8036
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
8037
|
+
aliases: [],
|
|
8038
|
+
_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.",
|
|
8039
|
+
smartRules: [
|
|
8040
|
+
{
|
|
8041
|
+
name: "shield:redis:block-flushall",
|
|
8042
|
+
tool: "*",
|
|
8043
|
+
conditions: [
|
|
8044
|
+
{
|
|
8045
|
+
field: "command",
|
|
8046
|
+
op: "matches",
|
|
8047
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
8048
|
+
flags: "i"
|
|
8049
|
+
}
|
|
8050
|
+
],
|
|
8051
|
+
verdict: "block",
|
|
8052
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
8053
|
+
},
|
|
8054
|
+
{
|
|
8055
|
+
name: "shield:redis:block-flushdb",
|
|
8056
|
+
tool: "*",
|
|
8057
|
+
conditions: [
|
|
8058
|
+
{
|
|
8059
|
+
field: "command",
|
|
8060
|
+
op: "matches",
|
|
8061
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
8062
|
+
flags: "i"
|
|
8063
|
+
}
|
|
8064
|
+
],
|
|
8065
|
+
verdict: "block",
|
|
8066
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
8067
|
+
},
|
|
8068
|
+
{
|
|
8069
|
+
name: "shield:redis:block-config-resetstat",
|
|
8070
|
+
tool: "*",
|
|
8071
|
+
conditions: [
|
|
8072
|
+
{
|
|
8073
|
+
field: "command",
|
|
8074
|
+
op: "matches",
|
|
8075
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
8076
|
+
flags: "i"
|
|
8077
|
+
}
|
|
8078
|
+
],
|
|
8079
|
+
verdict: "block",
|
|
8080
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
8081
|
+
},
|
|
8082
|
+
{
|
|
8083
|
+
name: "shield:redis:review-config-set",
|
|
8084
|
+
tool: "*",
|
|
8085
|
+
conditions: [
|
|
8086
|
+
{
|
|
8087
|
+
field: "command",
|
|
8088
|
+
op: "matches",
|
|
8089
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
8090
|
+
flags: "i"
|
|
8091
|
+
}
|
|
8092
|
+
],
|
|
8093
|
+
verdict: "review",
|
|
8094
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
8095
|
+
},
|
|
8096
|
+
{
|
|
8097
|
+
name: "shield:redis:review-del-wildcard",
|
|
8098
|
+
tool: "*",
|
|
8099
|
+
conditions: [
|
|
8100
|
+
{
|
|
8101
|
+
field: "command",
|
|
8102
|
+
op: "matches",
|
|
8103
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
8104
|
+
flags: "i"
|
|
8105
|
+
}
|
|
8106
|
+
],
|
|
8107
|
+
verdict: "review",
|
|
8108
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
8109
|
+
}
|
|
8110
|
+
],
|
|
8111
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
8112
|
+
};
|
|
8113
|
+
finance_default = {
|
|
8114
|
+
name: "finance",
|
|
8115
|
+
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.",
|
|
8116
|
+
aliases: ["payments", "money"],
|
|
8117
|
+
smartRules: [
|
|
8118
|
+
{
|
|
8119
|
+
name: "shield:finance:review-refund",
|
|
8120
|
+
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
8121
|
+
conditions: [],
|
|
8122
|
+
verdict: "review",
|
|
8123
|
+
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
8124
|
+
},
|
|
8125
|
+
{
|
|
8126
|
+
name: "shield:finance:review-payout",
|
|
8127
|
+
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
8128
|
+
conditions: [],
|
|
8129
|
+
verdict: "review",
|
|
8130
|
+
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
8131
|
+
},
|
|
8132
|
+
{
|
|
8133
|
+
name: "shield:finance:review-transfer",
|
|
8134
|
+
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
|
|
8135
|
+
conditions: [],
|
|
8136
|
+
verdict: "review",
|
|
8137
|
+
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
8138
|
+
},
|
|
8139
|
+
{
|
|
8140
|
+
name: "shield:finance:review-charge",
|
|
8141
|
+
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}",
|
|
8142
|
+
conditions: [],
|
|
8143
|
+
verdict: "review",
|
|
8144
|
+
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
8145
|
+
},
|
|
8146
|
+
{
|
|
8147
|
+
name: "shield:finance:review-payment-link",
|
|
8148
|
+
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
8149
|
+
conditions: [],
|
|
8150
|
+
verdict: "review",
|
|
8151
|
+
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
8152
|
+
},
|
|
8153
|
+
{
|
|
8154
|
+
name: "shield:finance:review-subscription",
|
|
8155
|
+
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
8156
|
+
conditions: [],
|
|
8157
|
+
verdict: "review",
|
|
8158
|
+
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
8159
|
+
},
|
|
8160
|
+
{
|
|
8161
|
+
name: "shield:finance:review-dispute",
|
|
8162
|
+
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
8163
|
+
conditions: [],
|
|
8164
|
+
verdict: "review",
|
|
8165
|
+
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
8166
|
+
},
|
|
8167
|
+
{
|
|
8168
|
+
name: "shield:finance:review-card",
|
|
8169
|
+
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
8170
|
+
conditions: [],
|
|
8171
|
+
verdict: "review",
|
|
8172
|
+
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
8173
|
+
},
|
|
8174
|
+
{
|
|
8175
|
+
name: "shield:finance:review-withdraw",
|
|
8176
|
+
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
8177
|
+
conditions: [],
|
|
8178
|
+
verdict: "review",
|
|
8179
|
+
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
8180
|
+
}
|
|
8181
|
+
],
|
|
8182
|
+
dangerousWords: []
|
|
8183
|
+
};
|
|
8184
|
+
identity_default = {
|
|
8185
|
+
name: "identity",
|
|
8186
|
+
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.",
|
|
8187
|
+
aliases: ["access", "iam"],
|
|
8188
|
+
smartRules: [
|
|
8189
|
+
{
|
|
8190
|
+
name: "shield:identity:review-password",
|
|
8191
|
+
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
8192
|
+
conditions: [],
|
|
8193
|
+
verdict: "review",
|
|
8194
|
+
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
8195
|
+
},
|
|
8196
|
+
{
|
|
8197
|
+
name: "shield:identity:review-key",
|
|
8198
|
+
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}",
|
|
8199
|
+
conditions: [],
|
|
8200
|
+
verdict: "review",
|
|
8201
|
+
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
8202
|
+
},
|
|
8203
|
+
{
|
|
8204
|
+
name: "shield:identity:review-access",
|
|
8205
|
+
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}",
|
|
8206
|
+
conditions: [],
|
|
8207
|
+
verdict: "review",
|
|
8208
|
+
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
8209
|
+
},
|
|
8210
|
+
{
|
|
8211
|
+
name: "shield:identity:review-membership",
|
|
8212
|
+
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}",
|
|
8213
|
+
conditions: [],
|
|
8214
|
+
verdict: "review",
|
|
8215
|
+
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
8216
|
+
},
|
|
8217
|
+
{
|
|
8218
|
+
name: "shield:identity:review-user",
|
|
8219
|
+
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}",
|
|
8220
|
+
conditions: [],
|
|
7838
8221
|
verdict: "review",
|
|
7839
|
-
reason: "
|
|
8222
|
+
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
8223
|
+
},
|
|
8224
|
+
{
|
|
8225
|
+
name: "shield:identity:review-mfa",
|
|
8226
|
+
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
8227
|
+
conditions: [],
|
|
8228
|
+
verdict: "review",
|
|
8229
|
+
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
7840
8230
|
}
|
|
7841
8231
|
],
|
|
7842
8232
|
dangerousWords: []
|
|
@@ -8010,103 +8400,510 @@ var init_dist = __esm({
|
|
|
8010
8400
|
],
|
|
8011
8401
|
dangerousWords: []
|
|
8012
8402
|
};
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
{
|
|
8038
|
-
field: "command",
|
|
8039
|
-
op: "matches",
|
|
8040
|
-
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
8041
|
-
flags: "i"
|
|
8042
|
-
}
|
|
8043
|
-
],
|
|
8044
|
-
verdict: "block",
|
|
8045
|
-
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
8046
|
-
},
|
|
8047
|
-
{
|
|
8048
|
-
name: "shield:redis:block-config-resetstat",
|
|
8049
|
-
tool: "*",
|
|
8050
|
-
conditions: [
|
|
8051
|
-
{
|
|
8052
|
-
field: "command",
|
|
8053
|
-
op: "matches",
|
|
8054
|
-
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
8055
|
-
flags: "i"
|
|
8056
|
-
}
|
|
8057
|
-
],
|
|
8058
|
-
verdict: "block",
|
|
8059
|
-
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
8060
|
-
},
|
|
8061
|
-
{
|
|
8062
|
-
name: "shield:redis:review-config-set",
|
|
8063
|
-
tool: "*",
|
|
8064
|
-
conditions: [
|
|
8065
|
-
{
|
|
8066
|
-
field: "command",
|
|
8067
|
-
op: "matches",
|
|
8068
|
-
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
8069
|
-
flags: "i"
|
|
8070
|
-
}
|
|
8071
|
-
],
|
|
8072
|
-
verdict: "review",
|
|
8073
|
-
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
8074
|
-
},
|
|
8075
|
-
{
|
|
8076
|
-
name: "shield:redis:review-del-wildcard",
|
|
8077
|
-
tool: "*",
|
|
8078
|
-
conditions: [
|
|
8079
|
-
{
|
|
8080
|
-
field: "command",
|
|
8081
|
-
op: "matches",
|
|
8082
|
-
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
8083
|
-
flags: "i"
|
|
8084
|
-
}
|
|
8085
|
-
],
|
|
8086
|
-
verdict: "review",
|
|
8087
|
-
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
8088
|
-
}
|
|
8089
|
-
],
|
|
8090
|
-
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
8403
|
+
ACTION_TEMPLATES = {
|
|
8404
|
+
finance: finance_default.smartRules,
|
|
8405
|
+
identity: identity_default.smartRules,
|
|
8406
|
+
publishing: publishing_default.smartRules
|
|
8407
|
+
};
|
|
8408
|
+
ACTION_CHECKS = {
|
|
8409
|
+
finance: { checkId: "actions.money", slug: "money", label: "Money check" },
|
|
8410
|
+
identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
|
|
8411
|
+
publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
|
|
8412
|
+
};
|
|
8413
|
+
GAP = "[^;&|]{0,200}?";
|
|
8414
|
+
prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
|
|
8415
|
+
httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
|
|
8416
|
+
httpWrite = (methods) => ({
|
|
8417
|
+
field: "command",
|
|
8418
|
+
op: "matches",
|
|
8419
|
+
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)`
|
|
8420
|
+
});
|
|
8421
|
+
HTTP_WRITE = httpWrite("POST|DELETE");
|
|
8422
|
+
HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
|
|
8423
|
+
NOT_STRIPE_TEST = {
|
|
8424
|
+
field: "command",
|
|
8425
|
+
op: "notMatches",
|
|
8426
|
+
value: "\\b[sr]k_test_"
|
|
8091
8427
|
};
|
|
8428
|
+
postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
|
|
8429
|
+
STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
|
|
8430
|
+
APP_SHIELD_SPECS = [
|
|
8431
|
+
{
|
|
8432
|
+
name: "stripe",
|
|
8433
|
+
label: "Stripe",
|
|
8434
|
+
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.",
|
|
8435
|
+
template: "finance",
|
|
8436
|
+
server: "*stripe*",
|
|
8437
|
+
doors: [
|
|
8438
|
+
{
|
|
8439
|
+
name: "shield:stripe:review-cli",
|
|
8440
|
+
tool: "bash",
|
|
8441
|
+
conditions: [
|
|
8442
|
+
{
|
|
8443
|
+
field: "command",
|
|
8444
|
+
op: "matches",
|
|
8445
|
+
value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
|
|
8446
|
+
flags: "i"
|
|
8447
|
+
},
|
|
8448
|
+
{
|
|
8449
|
+
field: "command",
|
|
8450
|
+
op: "matches",
|
|
8451
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8452
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8453
|
+
}
|
|
8454
|
+
],
|
|
8455
|
+
conditionMode: "all",
|
|
8456
|
+
verdict: "review",
|
|
8457
|
+
reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
|
|
8458
|
+
},
|
|
8459
|
+
{
|
|
8460
|
+
name: "shield:stripe:review-cli-request",
|
|
8461
|
+
tool: "bash",
|
|
8462
|
+
conditions: [
|
|
8463
|
+
{
|
|
8464
|
+
field: "command",
|
|
8465
|
+
op: "matches",
|
|
8466
|
+
value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
|
|
8467
|
+
flags: "i"
|
|
8468
|
+
},
|
|
8469
|
+
{
|
|
8470
|
+
field: "command",
|
|
8471
|
+
op: "matches",
|
|
8472
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
8473
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
8474
|
+
}
|
|
8475
|
+
],
|
|
8476
|
+
conditionMode: "all",
|
|
8477
|
+
verdict: "review",
|
|
8478
|
+
reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
|
|
8479
|
+
},
|
|
8480
|
+
{
|
|
8481
|
+
name: "shield:stripe:review-api",
|
|
8482
|
+
tool: "bash",
|
|
8483
|
+
conditions: [
|
|
8484
|
+
{
|
|
8485
|
+
field: "command",
|
|
8486
|
+
op: "matches",
|
|
8487
|
+
value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
|
|
8488
|
+
flags: "i"
|
|
8489
|
+
},
|
|
8490
|
+
HTTP_WRITE,
|
|
8491
|
+
NOT_STRIPE_TEST
|
|
8492
|
+
],
|
|
8493
|
+
conditionMode: "all",
|
|
8494
|
+
verdict: "review",
|
|
8495
|
+
reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
|
|
8496
|
+
},
|
|
8497
|
+
{
|
|
8498
|
+
name: "shield:stripe:review-code",
|
|
8499
|
+
tool: "bash",
|
|
8500
|
+
conditions: [
|
|
8501
|
+
{
|
|
8502
|
+
field: "command",
|
|
8503
|
+
op: "matches",
|
|
8504
|
+
// Python: stripe.Refund.create(...)
|
|
8505
|
+
value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
|
|
8506
|
+
},
|
|
8507
|
+
NOT_STRIPE_TEST
|
|
8508
|
+
],
|
|
8509
|
+
conditionMode: "all",
|
|
8510
|
+
verdict: "review",
|
|
8511
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8512
|
+
},
|
|
8513
|
+
{
|
|
8514
|
+
name: "shield:stripe:review-code-node",
|
|
8515
|
+
tool: "bash",
|
|
8516
|
+
conditions: [
|
|
8517
|
+
{
|
|
8518
|
+
field: "command",
|
|
8519
|
+
op: "matches",
|
|
8520
|
+
// Node: stripe.refunds.create(...)
|
|
8521
|
+
value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
|
|
8522
|
+
},
|
|
8523
|
+
NOT_STRIPE_TEST
|
|
8524
|
+
],
|
|
8525
|
+
conditionMode: "all",
|
|
8526
|
+
verdict: "review",
|
|
8527
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
8528
|
+
},
|
|
8529
|
+
{
|
|
8530
|
+
name: "shield:stripe:review-code-http",
|
|
8531
|
+
tool: "bash",
|
|
8532
|
+
conditions: [
|
|
8533
|
+
{
|
|
8534
|
+
field: "command",
|
|
8535
|
+
op: "matches",
|
|
8536
|
+
// requests.post("https://api.stripe.com/v1/refunds", ...)
|
|
8537
|
+
value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
|
|
8538
|
+
},
|
|
8539
|
+
NOT_STRIPE_TEST
|
|
8540
|
+
],
|
|
8541
|
+
conditionMode: "all",
|
|
8542
|
+
verdict: "review",
|
|
8543
|
+
reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
|
|
8544
|
+
}
|
|
8545
|
+
]
|
|
8546
|
+
},
|
|
8547
|
+
{
|
|
8548
|
+
name: "paypal",
|
|
8549
|
+
label: "PayPal",
|
|
8550
|
+
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.",
|
|
8551
|
+
template: "finance",
|
|
8552
|
+
server: "*paypal*",
|
|
8553
|
+
doors: [
|
|
8554
|
+
{
|
|
8555
|
+
name: "shield:paypal:review-api",
|
|
8556
|
+
tool: "bash",
|
|
8557
|
+
conditions: [
|
|
8558
|
+
{
|
|
8559
|
+
field: "command",
|
|
8560
|
+
op: "matches",
|
|
8561
|
+
// The live hosts only: api.paypal.com and api-m.paypal.com. The
|
|
8562
|
+
// sandbox (api-m.sandbox.paypal.com) moves no money.
|
|
8563
|
+
value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
|
|
8564
|
+
flags: "i"
|
|
8565
|
+
},
|
|
8566
|
+
{
|
|
8567
|
+
field: "command",
|
|
8568
|
+
op: "matches",
|
|
8569
|
+
// The endpoints that move money.
|
|
8570
|
+
value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
|
|
8571
|
+
flags: "i"
|
|
8572
|
+
},
|
|
8573
|
+
HTTP_WRITE
|
|
8574
|
+
],
|
|
8575
|
+
conditionMode: "all",
|
|
8576
|
+
verdict: "review",
|
|
8577
|
+
reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
|
|
8578
|
+
},
|
|
8579
|
+
{
|
|
8580
|
+
name: "shield:paypal:review-code",
|
|
8581
|
+
tool: "bash",
|
|
8582
|
+
conditions: [
|
|
8583
|
+
{
|
|
8584
|
+
field: "command",
|
|
8585
|
+
op: "matches",
|
|
8586
|
+
// The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
|
|
8587
|
+
// Python paypalrestsdk.
|
|
8588
|
+
value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
|
|
8589
|
+
}
|
|
8590
|
+
],
|
|
8591
|
+
verdict: "review",
|
|
8592
|
+
reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
|
|
8593
|
+
},
|
|
8594
|
+
{
|
|
8595
|
+
name: "shield:paypal:review-code-http",
|
|
8596
|
+
tool: "bash",
|
|
8597
|
+
conditions: [
|
|
8598
|
+
{
|
|
8599
|
+
field: "command",
|
|
8600
|
+
op: "matches",
|
|
8601
|
+
// The endpoints that move money; the token call every script
|
|
8602
|
+
// starts with (/v1/oauth2/token) is not one.
|
|
8603
|
+
value: postTo(
|
|
8604
|
+
"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)"
|
|
8605
|
+
)
|
|
8606
|
+
}
|
|
8607
|
+
],
|
|
8608
|
+
verdict: "review",
|
|
8609
|
+
reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
|
|
8610
|
+
}
|
|
8611
|
+
]
|
|
8612
|
+
},
|
|
8613
|
+
{
|
|
8614
|
+
name: "square",
|
|
8615
|
+
label: "Square",
|
|
8616
|
+
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.",
|
|
8617
|
+
template: "finance",
|
|
8618
|
+
// Square's servers, never Squarespace (a website builder).
|
|
8619
|
+
server: "{*square,*square[-_]*,*squareup*}",
|
|
8620
|
+
mcpTools: [
|
|
8621
|
+
{
|
|
8622
|
+
// Square's official MCP server has one tool for every API call; the
|
|
8623
|
+
// service and method name what it does.
|
|
8624
|
+
name: "shield:square:review-mcp-request",
|
|
8625
|
+
tool: "{,mcp__*__}make_api_request",
|
|
8626
|
+
conditions: [
|
|
8627
|
+
{
|
|
8628
|
+
field: "service",
|
|
8629
|
+
op: "matches",
|
|
8630
|
+
value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
|
|
8631
|
+
flags: "i"
|
|
8632
|
+
},
|
|
8633
|
+
{
|
|
8634
|
+
field: "method",
|
|
8635
|
+
op: "notMatches",
|
|
8636
|
+
value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
|
|
8637
|
+
flags: "i"
|
|
8638
|
+
}
|
|
8639
|
+
],
|
|
8640
|
+
conditionMode: "all",
|
|
8641
|
+
verdict: "review",
|
|
8642
|
+
reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
|
|
8643
|
+
}
|
|
8644
|
+
],
|
|
8645
|
+
doors: [
|
|
8646
|
+
{
|
|
8647
|
+
name: "shield:square:review-api",
|
|
8648
|
+
tool: "bash",
|
|
8649
|
+
conditions: [
|
|
8650
|
+
{
|
|
8651
|
+
field: "command",
|
|
8652
|
+
op: "matches",
|
|
8653
|
+
// The live host only; the sandbox is connect.squareupsandbox.com.
|
|
8654
|
+
value: httpTo(
|
|
8655
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8656
|
+
),
|
|
8657
|
+
flags: "i"
|
|
8658
|
+
},
|
|
8659
|
+
HTTP_WRITE_ANY
|
|
8660
|
+
],
|
|
8661
|
+
conditionMode: "all",
|
|
8662
|
+
verdict: "review",
|
|
8663
|
+
reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
|
|
8664
|
+
},
|
|
8665
|
+
{
|
|
8666
|
+
name: "shield:square:review-code",
|
|
8667
|
+
tool: "bash",
|
|
8668
|
+
conditions: [
|
|
8669
|
+
{
|
|
8670
|
+
field: "command",
|
|
8671
|
+
op: "matches",
|
|
8672
|
+
// The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
|
|
8673
|
+
value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
|
|
8674
|
+
},
|
|
8675
|
+
{
|
|
8676
|
+
field: "command",
|
|
8677
|
+
op: "matches",
|
|
8678
|
+
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*\\("
|
|
8679
|
+
}
|
|
8680
|
+
],
|
|
8681
|
+
conditionMode: "all",
|
|
8682
|
+
verdict: "review",
|
|
8683
|
+
reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
|
|
8684
|
+
},
|
|
8685
|
+
{
|
|
8686
|
+
name: "shield:square:review-code-http",
|
|
8687
|
+
tool: "bash",
|
|
8688
|
+
conditions: [
|
|
8689
|
+
{
|
|
8690
|
+
field: "command",
|
|
8691
|
+
op: "matches",
|
|
8692
|
+
value: postTo(
|
|
8693
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
8694
|
+
)
|
|
8695
|
+
}
|
|
8696
|
+
],
|
|
8697
|
+
verdict: "review",
|
|
8698
|
+
reason: "This code posts to the Square API. node9 asks before money moves (Square)."
|
|
8699
|
+
}
|
|
8700
|
+
]
|
|
8701
|
+
},
|
|
8702
|
+
{
|
|
8703
|
+
name: "plaid",
|
|
8704
|
+
label: "Plaid",
|
|
8705
|
+
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.",
|
|
8706
|
+
template: "finance",
|
|
8707
|
+
server: "*plaid*",
|
|
8708
|
+
doors: [
|
|
8709
|
+
{
|
|
8710
|
+
name: "shield:plaid:review-api",
|
|
8711
|
+
tool: "bash",
|
|
8712
|
+
conditions: [
|
|
8713
|
+
{
|
|
8714
|
+
field: "command",
|
|
8715
|
+
op: "matches",
|
|
8716
|
+
// Every Plaid endpoint is a POST, so the path is what tells a
|
|
8717
|
+
// write; the live host only, never sandbox.plaid.com. A script
|
|
8718
|
+
// that posts to the URL meets this door too.
|
|
8719
|
+
value: httpTo(
|
|
8720
|
+
"production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
|
|
8721
|
+
),
|
|
8722
|
+
flags: "i"
|
|
8723
|
+
}
|
|
8724
|
+
],
|
|
8725
|
+
verdict: "review",
|
|
8726
|
+
reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
|
|
8727
|
+
},
|
|
8728
|
+
{
|
|
8729
|
+
name: "shield:plaid:review-code",
|
|
8730
|
+
tool: "bash",
|
|
8731
|
+
conditions: [
|
|
8732
|
+
{ field: "command", op: "matches", value: "\\bplaid", flags: "i" },
|
|
8733
|
+
{
|
|
8734
|
+
field: "command",
|
|
8735
|
+
op: "matches",
|
|
8736
|
+
value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
|
|
8737
|
+
}
|
|
8738
|
+
],
|
|
8739
|
+
conditionMode: "all",
|
|
8740
|
+
verdict: "review",
|
|
8741
|
+
reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
|
|
8742
|
+
}
|
|
8743
|
+
]
|
|
8744
|
+
},
|
|
8745
|
+
{
|
|
8746
|
+
name: "okta",
|
|
8747
|
+
label: "Okta",
|
|
8748
|
+
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.",
|
|
8749
|
+
template: "identity",
|
|
8750
|
+
server: "*okta*",
|
|
8751
|
+
doors: [
|
|
8752
|
+
{
|
|
8753
|
+
name: "shield:okta:review-api",
|
|
8754
|
+
tool: "bash",
|
|
8755
|
+
conditions: [
|
|
8756
|
+
{
|
|
8757
|
+
field: "command",
|
|
8758
|
+
op: "matches",
|
|
8759
|
+
// The management API only: sign-in (/api/v1/authn, /sessions,
|
|
8760
|
+
// /oauth2) is not under these paths.
|
|
8761
|
+
value: httpTo(
|
|
8762
|
+
"[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
|
|
8763
|
+
),
|
|
8764
|
+
flags: "i"
|
|
8765
|
+
},
|
|
8766
|
+
HTTP_WRITE_ANY
|
|
8767
|
+
],
|
|
8768
|
+
conditionMode: "all",
|
|
8769
|
+
verdict: "review",
|
|
8770
|
+
reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
|
|
8771
|
+
},
|
|
8772
|
+
{
|
|
8773
|
+
name: "shield:okta:review-code",
|
|
8774
|
+
tool: "bash",
|
|
8775
|
+
conditions: [
|
|
8776
|
+
{
|
|
8777
|
+
field: "command",
|
|
8778
|
+
op: "matches",
|
|
8779
|
+
// The Okta SDK in the script: a method named createUser alone is
|
|
8780
|
+
// in half the applications ever written.
|
|
8781
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8782
|
+
},
|
|
8783
|
+
{
|
|
8784
|
+
field: "command",
|
|
8785
|
+
op: "matches",
|
|
8786
|
+
value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
|
|
8787
|
+
}
|
|
8788
|
+
],
|
|
8789
|
+
conditionMode: "all",
|
|
8790
|
+
verdict: "review",
|
|
8791
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8792
|
+
},
|
|
8793
|
+
{
|
|
8794
|
+
name: "shield:okta:review-code-py",
|
|
8795
|
+
tool: "bash",
|
|
8796
|
+
conditions: [
|
|
8797
|
+
{
|
|
8798
|
+
field: "command",
|
|
8799
|
+
op: "matches",
|
|
8800
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
8801
|
+
},
|
|
8802
|
+
{
|
|
8803
|
+
field: "command",
|
|
8804
|
+
op: "matches",
|
|
8805
|
+
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*\\("
|
|
8806
|
+
}
|
|
8807
|
+
],
|
|
8808
|
+
conditionMode: "all",
|
|
8809
|
+
verdict: "review",
|
|
8810
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
8811
|
+
}
|
|
8812
|
+
]
|
|
8813
|
+
},
|
|
8814
|
+
{
|
|
8815
|
+
name: "auth0",
|
|
8816
|
+
label: "Auth0",
|
|
8817
|
+
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.",
|
|
8818
|
+
template: "identity",
|
|
8819
|
+
server: "*auth0*",
|
|
8820
|
+
mcpTools: [
|
|
8821
|
+
{
|
|
8822
|
+
// Auth0's official MCP server: applications (their secrets and
|
|
8823
|
+
// callbacks), APIs and their scopes, and Actions (code that runs at
|
|
8824
|
+
// every sign-in).
|
|
8825
|
+
name: "shield:auth0:review-tenant-config",
|
|
8826
|
+
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}",
|
|
8827
|
+
conditions: [],
|
|
8828
|
+
verdict: "review",
|
|
8829
|
+
reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
|
|
8830
|
+
}
|
|
8831
|
+
],
|
|
8832
|
+
doors: [
|
|
8833
|
+
{
|
|
8834
|
+
name: "shield:auth0:review-cli",
|
|
8835
|
+
tool: "bash",
|
|
8836
|
+
conditions: [
|
|
8837
|
+
{
|
|
8838
|
+
field: "command",
|
|
8839
|
+
op: "matches",
|
|
8840
|
+
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-])",
|
|
8841
|
+
flags: "i"
|
|
8842
|
+
}
|
|
8843
|
+
],
|
|
8844
|
+
verdict: "review",
|
|
8845
|
+
reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8846
|
+
},
|
|
8847
|
+
{
|
|
8848
|
+
name: "shield:auth0:review-api",
|
|
8849
|
+
tool: "bash",
|
|
8850
|
+
conditions: [
|
|
8851
|
+
{
|
|
8852
|
+
field: "command",
|
|
8853
|
+
op: "matches",
|
|
8854
|
+
// The Management API only; /oauth/token is signing in.
|
|
8855
|
+
value: httpTo(
|
|
8856
|
+
"\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
|
|
8857
|
+
),
|
|
8858
|
+
flags: "i"
|
|
8859
|
+
},
|
|
8860
|
+
HTTP_WRITE_ANY
|
|
8861
|
+
],
|
|
8862
|
+
conditionMode: "all",
|
|
8863
|
+
verdict: "review",
|
|
8864
|
+
reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
|
|
8865
|
+
},
|
|
8866
|
+
{
|
|
8867
|
+
name: "shield:auth0:review-code",
|
|
8868
|
+
tool: "bash",
|
|
8869
|
+
conditions: [
|
|
8870
|
+
{
|
|
8871
|
+
field: "command",
|
|
8872
|
+
op: "matches",
|
|
8873
|
+
// The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
|
|
8874
|
+
value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
|
|
8875
|
+
},
|
|
8876
|
+
{
|
|
8877
|
+
field: "command",
|
|
8878
|
+
op: "matches",
|
|
8879
|
+
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*\\("
|
|
8880
|
+
}
|
|
8881
|
+
],
|
|
8882
|
+
conditionMode: "all",
|
|
8883
|
+
verdict: "review",
|
|
8884
|
+
reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
|
|
8885
|
+
}
|
|
8886
|
+
]
|
|
8887
|
+
}
|
|
8888
|
+
];
|
|
8889
|
+
APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
|
|
8890
|
+
APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
|
|
8891
|
+
APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
|
|
8892
|
+
GENERAL_ACTION_RULES = buildGeneralRules();
|
|
8092
8893
|
BUILTIN_SHIELDS = {
|
|
8093
8894
|
[aws_default.name]: aws_default,
|
|
8094
8895
|
[bash_safe_default.name]: bash_safe_default,
|
|
8095
8896
|
[docker_default.name]: docker_default,
|
|
8096
8897
|
[filesystem_default.name]: filesystem_default,
|
|
8097
|
-
// Dangerous actions as category shields (node9Firewall#385): an MCP app's
|
|
8098
|
-
// money, credential and publishing tools, asked about whatever the request
|
|
8099
|
-
// said. One glob per rule covers the bare name the gateway sends and the
|
|
8100
|
-
// mcp__<server>__ name the agent hook sends.
|
|
8101
|
-
[finance_default.name]: finance_default,
|
|
8102
8898
|
[github_default.name]: github_default,
|
|
8103
|
-
[identity_default.name]: identity_default,
|
|
8104
8899
|
[k8s_default.name]: k8s_default,
|
|
8105
8900
|
[mongodb_default.name]: mongodb_default,
|
|
8106
8901
|
[postgres_default.name]: postgres_default,
|
|
8107
8902
|
[project_jail_default.name]: project_jail_default,
|
|
8108
|
-
[
|
|
8109
|
-
|
|
8903
|
+
[redis_default.name]: redis_default,
|
|
8904
|
+
// App shields generated from the action templates (node9Firewall#390): one
|
|
8905
|
+
// per app, every door (MCP server, CLI, API, library), off by default.
|
|
8906
|
+
...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
|
|
8110
8907
|
};
|
|
8111
8908
|
assertBuiltinShieldRegexesAreSafe();
|
|
8112
8909
|
SSRF_MAX_HOST = 253;
|
|
@@ -8162,9 +8959,9 @@ var init_dist = __esm({
|
|
|
8162
8959
|
},
|
|
8163
8960
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
8164
8961
|
"commands.inline-exec": {
|
|
8165
|
-
plain: "The agent runs a small program written inside the command itself.",
|
|
8962
|
+
plain: "The agent runs a small program written inside the command itself, and node9 reads it: ordinary code runs, code that deletes, sends data out, runs other commands or reads protected files is held, and so is code node9 cannot read.",
|
|
8166
8963
|
example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
|
|
8167
|
-
advice: "
|
|
8964
|
+
advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
|
|
8168
8965
|
},
|
|
8169
8966
|
"commands.eval-dynamic": {
|
|
8170
8967
|
plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
|
|
@@ -8196,6 +8993,21 @@ var init_dist = __esm({
|
|
|
8196
8993
|
example: "chmod 777 deploy.sh",
|
|
8197
8994
|
advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
|
|
8198
8995
|
},
|
|
8996
|
+
"actions.money": {
|
|
8997
|
+
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.",
|
|
8998
|
+
example: "refund_order on a server called acme-billing",
|
|
8999
|
+
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."
|
|
9000
|
+
},
|
|
9001
|
+
"actions.access": {
|
|
9002
|
+
plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
|
|
9003
|
+
example: "invite_user or grant_role on a company admin server",
|
|
9004
|
+
advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
|
|
9005
|
+
},
|
|
9006
|
+
"actions.publish": {
|
|
9007
|
+
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.",
|
|
9008
|
+
example: "create_release or deploy_to_production on a company MCP server",
|
|
9009
|
+
advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
|
|
9010
|
+
},
|
|
8199
9011
|
"commands.sudo": {
|
|
8200
9012
|
plain: "The agent runs a command as the administrator (root).",
|
|
8201
9013
|
example: "sudo apt-get install -y nginx",
|
|
@@ -8356,7 +9168,7 @@ var init_dist = __esm({
|
|
|
8356
9168
|
advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
|
|
8357
9169
|
},
|
|
8358
9170
|
"loading.malicious-package": {
|
|
8359
|
-
plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
|
|
9171
|
+
plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only. A new release built from the same repository as the one before it (registry provenance) and an install script the package has always run do not stop; a release that lost the provenance its previous release had does.",
|
|
8360
9172
|
example: "npm install of a package flagged as malware",
|
|
8361
9173
|
advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
|
|
8362
9174
|
}
|
|
@@ -8408,7 +9220,7 @@ var init_dist = __esm({
|
|
|
8408
9220
|
id: "commands.inline-exec",
|
|
8409
9221
|
group: "commands",
|
|
8410
9222
|
title: "Inline code execution",
|
|
8411
|
-
catches: "
|
|
9223
|
+
catches: "inline code (python -c, node -e, a heredoc into an interpreter) that deletes, sends, opens a socket, runs commands, reads protected files or decodes and runs; unreadable inline code",
|
|
8412
9224
|
values: ALL,
|
|
8413
9225
|
defaultValue: "review",
|
|
8414
9226
|
legacy: ["Node9 Standard (Inline Execution)"]
|
|
@@ -8765,6 +9577,38 @@ var init_dist = __esm({
|
|
|
8765
9577
|
values: ALL,
|
|
8766
9578
|
defaultValue: "block",
|
|
8767
9579
|
legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
|
|
9580
|
+
},
|
|
9581
|
+
// ── Actions in apps (node9Firewall#390) ──────────────────────────────────
|
|
9582
|
+
// The safety net under the app shields: an action that moves money, changes
|
|
9583
|
+
// access or publishes, on an MCP server no app shield claims (an internal
|
|
9584
|
+
// billing server, a vendor with no shield, Zapier). Review by default; the
|
|
9585
|
+
// rules come from the action templates (shields/apps.ts).
|
|
9586
|
+
{
|
|
9587
|
+
id: "actions.money",
|
|
9588
|
+
group: "actions",
|
|
9589
|
+
title: "Money moves",
|
|
9590
|
+
catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
|
|
9591
|
+
values: ALL,
|
|
9592
|
+
defaultValue: "review",
|
|
9593
|
+
legacy: generalActionRuleNames("finance")
|
|
9594
|
+
},
|
|
9595
|
+
{
|
|
9596
|
+
id: "actions.access",
|
|
9597
|
+
group: "actions",
|
|
9598
|
+
title: "Accounts and access change",
|
|
9599
|
+
catches: "a password, key, role, permission or user changed through an app with no protection of its own",
|
|
9600
|
+
values: ALL,
|
|
9601
|
+
defaultValue: "review",
|
|
9602
|
+
legacy: generalActionRuleNames("identity")
|
|
9603
|
+
},
|
|
9604
|
+
{
|
|
9605
|
+
id: "actions.publish",
|
|
9606
|
+
group: "actions",
|
|
9607
|
+
title: "Something is published",
|
|
9608
|
+
catches: "a release, package, deploy or public post through an app with no protection of its own",
|
|
9609
|
+
values: ALL,
|
|
9610
|
+
defaultValue: "review",
|
|
9611
|
+
legacy: generalActionRuleNames("publishing")
|
|
8768
9612
|
}
|
|
8769
9613
|
];
|
|
8770
9614
|
SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
|
|
@@ -8887,12 +9731,223 @@ var init_dist = __esm({
|
|
|
8887
9731
|
\u03A5: "Y",
|
|
8888
9732
|
\u03A7: "X"
|
|
8889
9733
|
};
|
|
8890
|
-
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
8891
|
-
MAX_BLOBS = 32;
|
|
8892
|
-
MIN_DECODED = 16;
|
|
8893
|
-
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
8894
|
-
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
8895
|
-
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
9734
|
+
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
9735
|
+
MAX_BLOBS = 32;
|
|
9736
|
+
MIN_DECODED = 16;
|
|
9737
|
+
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
9738
|
+
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
9739
|
+
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
9740
|
+
SYSTEM_AUTH_FILES = [
|
|
9741
|
+
"/etc/passwd",
|
|
9742
|
+
"/etc/shadow",
|
|
9743
|
+
"/etc/group",
|
|
9744
|
+
"/etc/gshadow",
|
|
9745
|
+
"/etc/sudoers"
|
|
9746
|
+
];
|
|
9747
|
+
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9748
|
+
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9749
|
+
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9750
|
+
SYSTEM_DIRS = [
|
|
9751
|
+
"/usr",
|
|
9752
|
+
"/etc",
|
|
9753
|
+
"/boot",
|
|
9754
|
+
"/bin",
|
|
9755
|
+
"/sbin",
|
|
9756
|
+
"/lib",
|
|
9757
|
+
"/lib64",
|
|
9758
|
+
"/var/log"
|
|
9759
|
+
];
|
|
9760
|
+
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
9761
|
+
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
9762
|
+
AGENT_INSTRUCTION_FILES = [
|
|
9763
|
+
".gitlab-ci.yml",
|
|
9764
|
+
"CLAUDE.md",
|
|
9765
|
+
"AGENTS.md",
|
|
9766
|
+
".cursorrules",
|
|
9767
|
+
".mcp.json",
|
|
9768
|
+
".vscode/settings.json",
|
|
9769
|
+
"package.json"
|
|
9770
|
+
];
|
|
9771
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
9772
|
+
".github/workflows",
|
|
9773
|
+
".claude",
|
|
9774
|
+
".cursor",
|
|
9775
|
+
".git/hooks",
|
|
9776
|
+
".husky"
|
|
9777
|
+
];
|
|
9778
|
+
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
9779
|
+
MAX_INLINE_CODE = 64 * 1024;
|
|
9780
|
+
WHAT = {
|
|
9781
|
+
deletes: "deletes files",
|
|
9782
|
+
"writes-outside": "writes a file outside the project",
|
|
9783
|
+
sends: "sends data over the network",
|
|
9784
|
+
listens: "opens a listening socket or a shell",
|
|
9785
|
+
"runs-commands": "runs other commands or processes",
|
|
9786
|
+
"reads-protected": "reads a protected file",
|
|
9787
|
+
"builds-code": "runs code it builds at run time",
|
|
9788
|
+
permissions: "changes permissions or ownership",
|
|
9789
|
+
monitors: "captures the screen, the clipboard or the keyboard",
|
|
9790
|
+
amplifies: "writes or sends in an endless loop"
|
|
9791
|
+
};
|
|
9792
|
+
OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
|
|
9793
|
+
pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
|
|
9794
|
+
either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
|
|
9795
|
+
PYTHON = [
|
|
9796
|
+
[
|
|
9797
|
+
"deletes",
|
|
9798
|
+
/\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/
|
|
9799
|
+
],
|
|
9800
|
+
[
|
|
9801
|
+
"writes-outside",
|
|
9802
|
+
new RegExp(
|
|
9803
|
+
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}`
|
|
9804
|
+
)
|
|
9805
|
+
],
|
|
9806
|
+
[
|
|
9807
|
+
"sends",
|
|
9808
|
+
either(
|
|
9809
|
+
pyImport(
|
|
9810
|
+
String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
|
|
9811
|
+
),
|
|
9812
|
+
/\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
|
|
9813
|
+
)
|
|
9814
|
+
],
|
|
9815
|
+
[
|
|
9816
|
+
"listens",
|
|
9817
|
+
either(
|
|
9818
|
+
pyImport(String.raw`pty|socketserver|http\.server`),
|
|
9819
|
+
/\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
|
|
9820
|
+
)
|
|
9821
|
+
],
|
|
9822
|
+
[
|
|
9823
|
+
"runs-commands",
|
|
9824
|
+
either(
|
|
9825
|
+
pyImport("subprocess|pexpect|commands|psutil"),
|
|
9826
|
+
/\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+\*/
|
|
9827
|
+
)
|
|
9828
|
+
],
|
|
9829
|
+
[
|
|
9830
|
+
"builds-code",
|
|
9831
|
+
/(?<![\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*\[/
|
|
9832
|
+
],
|
|
9833
|
+
[
|
|
9834
|
+
// Spyware shapes: a screenshot, the clipboard, a key logger.
|
|
9835
|
+
"monitors",
|
|
9836
|
+
either(
|
|
9837
|
+
pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
|
|
9838
|
+
/\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
|
|
9839
|
+
)
|
|
9840
|
+
],
|
|
9841
|
+
[
|
|
9842
|
+
"permissions",
|
|
9843
|
+
/\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
|
|
9844
|
+
]
|
|
9845
|
+
];
|
|
9846
|
+
NODE = [
|
|
9847
|
+
[
|
|
9848
|
+
"deletes",
|
|
9849
|
+
/\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
|
|
9850
|
+
],
|
|
9851
|
+
[
|
|
9852
|
+
"writes-outside",
|
|
9853
|
+
new RegExp(
|
|
9854
|
+
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}`
|
|
9855
|
+
)
|
|
9856
|
+
],
|
|
9857
|
+
[
|
|
9858
|
+
"sends",
|
|
9859
|
+
/\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*\(/
|
|
9860
|
+
],
|
|
9861
|
+
["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
|
|
9862
|
+
[
|
|
9863
|
+
"runs-commands",
|
|
9864
|
+
/\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
|
|
9865
|
+
],
|
|
9866
|
+
[
|
|
9867
|
+
"builds-code",
|
|
9868
|
+
/(?<![\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)]/
|
|
9869
|
+
],
|
|
9870
|
+
[
|
|
9871
|
+
"permissions",
|
|
9872
|
+
/\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
|
|
9873
|
+
]
|
|
9874
|
+
];
|
|
9875
|
+
PERL = [
|
|
9876
|
+
["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
|
|
9877
|
+
[
|
|
9878
|
+
"writes-outside",
|
|
9879
|
+
new RegExp(
|
|
9880
|
+
String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
|
|
9881
|
+
)
|
|
9882
|
+
],
|
|
9883
|
+
[
|
|
9884
|
+
"sends",
|
|
9885
|
+
/\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
|
|
9886
|
+
],
|
|
9887
|
+
[
|
|
9888
|
+
"runs-commands",
|
|
9889
|
+
/\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
|
|
9890
|
+
],
|
|
9891
|
+
["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
|
|
9892
|
+
["permissions", /\b(?:chmod|chown)\b/]
|
|
9893
|
+
];
|
|
9894
|
+
RUBY = [
|
|
9895
|
+
[
|
|
9896
|
+
"deletes",
|
|
9897
|
+
/\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
|
|
9898
|
+
],
|
|
9899
|
+
[
|
|
9900
|
+
"writes-outside",
|
|
9901
|
+
new RegExp(
|
|
9902
|
+
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}`
|
|
9903
|
+
)
|
|
9904
|
+
],
|
|
9905
|
+
[
|
|
9906
|
+
"sends",
|
|
9907
|
+
/\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)['"]/
|
|
9908
|
+
],
|
|
9909
|
+
["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
|
|
9910
|
+
[
|
|
9911
|
+
"runs-commands",
|
|
9912
|
+
/(?<![\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/
|
|
9913
|
+
],
|
|
9914
|
+
[
|
|
9915
|
+
"builds-code",
|
|
9916
|
+
/(?<![\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/
|
|
9917
|
+
],
|
|
9918
|
+
["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
|
|
9919
|
+
];
|
|
9920
|
+
PHP = [
|
|
9921
|
+
["deletes", /\b(?:unlink|rmdir)\s*\(/],
|
|
9922
|
+
[
|
|
9923
|
+
"writes-outside",
|
|
9924
|
+
new RegExp(
|
|
9925
|
+
String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
|
|
9926
|
+
)
|
|
9927
|
+
],
|
|
9928
|
+
[
|
|
9929
|
+
"sends",
|
|
9930
|
+
/\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):/
|
|
9931
|
+
],
|
|
9932
|
+
["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
|
|
9933
|
+
[
|
|
9934
|
+
"runs-commands",
|
|
9935
|
+
/\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
|
|
9936
|
+
],
|
|
9937
|
+
[
|
|
9938
|
+
"builds-code",
|
|
9939
|
+
/\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
|
|
9940
|
+
],
|
|
9941
|
+
["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
|
|
9942
|
+
];
|
|
9943
|
+
TABLES = {
|
|
9944
|
+
python: PYTHON,
|
|
9945
|
+
node: NODE,
|
|
9946
|
+
perl: PERL,
|
|
9947
|
+
ruby: RUBY,
|
|
9948
|
+
php: PHP
|
|
9949
|
+
};
|
|
9950
|
+
STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
|
|
8896
9951
|
({ syntax } = mvdanSh);
|
|
8897
9952
|
sharedParser = syntax.NewParser();
|
|
8898
9953
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -8930,6 +9985,10 @@ var init_dist = __esm({
|
|
|
8930
9985
|
AST_CACHE_MAX = 5e3;
|
|
8931
9986
|
astCache = /* @__PURE__ */ new Map();
|
|
8932
9987
|
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
9988
|
+
INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
|
|
9989
|
+
PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
|
|
9990
|
+
ACCESS_VIEW_CACHE_MAX = 1e3;
|
|
9991
|
+
accessViewCache = /* @__PURE__ */ new Map();
|
|
8933
9992
|
MAX_READING_PAYLOADS = 8;
|
|
8934
9993
|
defaultExpansionOps = null;
|
|
8935
9994
|
VARIABLE_READERS = /* @__PURE__ */ new Set([
|
|
@@ -9639,6 +10698,14 @@ var init_dist = __esm({
|
|
|
9639
10698
|
INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
9640
10699
|
MAX_PAYLOAD_NESTING = 8;
|
|
9641
10700
|
payloadNesting = 0;
|
|
10701
|
+
MAX_INLINE_CALLS = 16;
|
|
10702
|
+
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;
|
|
10703
|
+
UNREADABLE_SOURCE = {
|
|
10704
|
+
pipe: "reads its code from another program through a pipe",
|
|
10705
|
+
file: "reads its code from a file on stdin",
|
|
10706
|
+
variable: "runs code the shell fills in at run time",
|
|
10707
|
+
payload: "runs a shell payload that evaluates, sources or runs further code"
|
|
10708
|
+
};
|
|
9642
10709
|
SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
|
|
9643
10710
|
SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
|
|
9644
10711
|
MAX_SCRIPT_OPERANDS = 8;
|
|
@@ -9838,9 +10905,6 @@ var init_dist = __esm({
|
|
|
9838
10905
|
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
9839
10906
|
NONE = { kind: "none" };
|
|
9840
10907
|
UNKNOWN = { kind: "unknown" };
|
|
9841
|
-
MAX_REGEX_LENGTH = 256;
|
|
9842
|
-
REGEX_CACHE_MAX = 500;
|
|
9843
|
-
regexCache = /* @__PURE__ */ new Map();
|
|
9844
10908
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
9845
10909
|
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9846
10910
|
"bash",
|
|
@@ -9955,45 +11019,6 @@ var init_dist = __esm({
|
|
|
9955
11019
|
(s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
|
|
9956
11020
|
)
|
|
9957
11021
|
);
|
|
9958
|
-
SYSTEM_AUTH_FILES = [
|
|
9959
|
-
"/etc/passwd",
|
|
9960
|
-
"/etc/shadow",
|
|
9961
|
-
"/etc/group",
|
|
9962
|
-
"/etc/gshadow",
|
|
9963
|
-
"/etc/sudoers"
|
|
9964
|
-
];
|
|
9965
|
-
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
9966
|
-
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
9967
|
-
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
9968
|
-
SYSTEM_DIRS = [
|
|
9969
|
-
"/usr",
|
|
9970
|
-
"/etc",
|
|
9971
|
-
"/boot",
|
|
9972
|
-
"/bin",
|
|
9973
|
-
"/sbin",
|
|
9974
|
-
"/lib",
|
|
9975
|
-
"/lib64",
|
|
9976
|
-
"/var/log"
|
|
9977
|
-
];
|
|
9978
|
-
SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
|
|
9979
|
-
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
9980
|
-
AGENT_INSTRUCTION_FILES = [
|
|
9981
|
-
".gitlab-ci.yml",
|
|
9982
|
-
"CLAUDE.md",
|
|
9983
|
-
"AGENTS.md",
|
|
9984
|
-
".cursorrules",
|
|
9985
|
-
".mcp.json",
|
|
9986
|
-
".vscode/settings.json",
|
|
9987
|
-
"package.json"
|
|
9988
|
-
];
|
|
9989
|
-
AGENT_INSTRUCTION_DIRS = [
|
|
9990
|
-
".github/workflows",
|
|
9991
|
-
".claude",
|
|
9992
|
-
".cursor",
|
|
9993
|
-
".git/hooks",
|
|
9994
|
-
".husky"
|
|
9995
|
-
];
|
|
9996
|
-
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
9997
11022
|
exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
|
|
9998
11023
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
9999
11024
|
BIP39_ENGLISH = `abandon
|
|
@@ -13486,7 +14511,11 @@ zoo`.split("\n");
|
|
|
13486
14511
|
"commands.disk-destroy",
|
|
13487
14512
|
"commands.dangerous-word",
|
|
13488
14513
|
"commands.node9-disarm",
|
|
13489
|
-
"data.pipe-chain"
|
|
14514
|
+
"data.pipe-chain",
|
|
14515
|
+
// The general action checks (node9Firewall#390): smart rules the map governs.
|
|
14516
|
+
"actions.money",
|
|
14517
|
+
"actions.access",
|
|
14518
|
+
"actions.publish"
|
|
13490
14519
|
]);
|
|
13491
14520
|
KNOB_GOVERNED = {
|
|
13492
14521
|
"data.secrets": ["off", "block"],
|
|
@@ -14194,7 +15223,8 @@ var SmartConditionSchema = z.object({
|
|
|
14194
15223
|
}
|
|
14195
15224
|
),
|
|
14196
15225
|
value: z.string().optional(),
|
|
14197
|
-
flags: z.string().optional()
|
|
15226
|
+
flags: z.string().optional(),
|
|
15227
|
+
reading: z.enum(["access"]).optional()
|
|
14198
15228
|
}).refine(
|
|
14199
15229
|
(c) => {
|
|
14200
15230
|
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
@@ -14205,6 +15235,9 @@ var SmartConditionSchema = z.object({
|
|
|
14205
15235
|
var SmartRuleSchema = z.object({
|
|
14206
15236
|
name: z.string().optional(),
|
|
14207
15237
|
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
15238
|
+
// Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
|
|
15239
|
+
server: z.string().min(1).optional(),
|
|
15240
|
+
notServer: z.string().min(1).optional(),
|
|
14208
15241
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
14209
15242
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
14210
15243
|
verdict: z.enum(["allow", "review", "block"], {
|
|
@@ -14354,8 +15387,8 @@ var ConfigFileSchema = z.object({
|
|
|
14354
15387
|
}).strict();
|
|
14355
15388
|
function formatIssues(issues) {
|
|
14356
15389
|
const lines = issues.map((issue) => {
|
|
14357
|
-
const
|
|
14358
|
-
return ` \u2022 ${
|
|
15390
|
+
const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
15391
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
14359
15392
|
});
|
|
14360
15393
|
return `Invalid config:
|
|
14361
15394
|
${lines.join("\n")}`;
|
|
@@ -14368,9 +15401,9 @@ function prunePaths(root, paths) {
|
|
|
14368
15401
|
const yi = y[y.length - 1];
|
|
14369
15402
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
14370
15403
|
});
|
|
14371
|
-
for (const
|
|
15404
|
+
for (const path28 of ordered) {
|
|
14372
15405
|
let cur = root;
|
|
14373
|
-
for (const key of
|
|
15406
|
+
for (const key of path28.slice(0, -1)) {
|
|
14374
15407
|
if (cur === null || typeof cur !== "object") {
|
|
14375
15408
|
cur = void 0;
|
|
14376
15409
|
break;
|
|
@@ -14378,7 +15411,7 @@ function prunePaths(root, paths) {
|
|
|
14378
15411
|
cur = cur[key];
|
|
14379
15412
|
}
|
|
14380
15413
|
if (cur === null || typeof cur !== "object") continue;
|
|
14381
|
-
const last =
|
|
15414
|
+
const last = path28[path28.length - 1];
|
|
14382
15415
|
if (Array.isArray(cur)) {
|
|
14383
15416
|
const i = Number(last);
|
|
14384
15417
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -14414,7 +15447,7 @@ function sanitizeConfig(raw) {
|
|
|
14414
15447
|
return issue.keys.map((k) => [...at, k]);
|
|
14415
15448
|
}
|
|
14416
15449
|
return [at.slice(0, -level || void 0)];
|
|
14417
|
-
}).filter((
|
|
15450
|
+
}).filter((path28) => path28.length > 0);
|
|
14418
15451
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
14419
15452
|
}
|
|
14420
15453
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -14440,6 +15473,85 @@ import fs2 from "fs";
|
|
|
14440
15473
|
import path2 from "path";
|
|
14441
15474
|
import os2 from "os";
|
|
14442
15475
|
init_dist();
|
|
15476
|
+
|
|
15477
|
+
// src/shields/build.ts
|
|
15478
|
+
init_dist();
|
|
15479
|
+
function escapeRegex(s) {
|
|
15480
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15481
|
+
}
|
|
15482
|
+
function slug(s) {
|
|
15483
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
15484
|
+
}
|
|
15485
|
+
var B = "[\\s/\\\\]";
|
|
15486
|
+
var SEP = "[/\\\\]";
|
|
15487
|
+
function pathToRegexFragment(rawPath) {
|
|
15488
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
15489
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
15490
|
+
if (segments2.length === 0) return "";
|
|
15491
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
15492
|
+
}
|
|
15493
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
15494
|
+
const value = pathToRegexFragment(rawPath);
|
|
15495
|
+
if (!value || !candidate) return false;
|
|
15496
|
+
const re = getCompiledRegex(value);
|
|
15497
|
+
if (!re) return false;
|
|
15498
|
+
return re.test(candidate);
|
|
15499
|
+
}
|
|
15500
|
+
function pathRules(rawPath, verdict, reason) {
|
|
15501
|
+
const value = pathToRegexFragment(rawPath);
|
|
15502
|
+
if (!value) return [];
|
|
15503
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
15504
|
+
const s = slug(rawPath);
|
|
15505
|
+
return [
|
|
15506
|
+
// `reading: 'access'`: a script's interpreter line and the program a
|
|
15507
|
+
// command runs are not reads of the jailed path (node9Firewall#385).
|
|
15508
|
+
{
|
|
15509
|
+
name: `${verdict}-path-${s}-bash`,
|
|
15510
|
+
tool: "bash",
|
|
15511
|
+
conditions: [{ field: "command", op: "matches", value, reading: "access" }],
|
|
15512
|
+
verdict,
|
|
15513
|
+
reason: why
|
|
15514
|
+
},
|
|
15515
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
15516
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
15517
|
+
{
|
|
15518
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
15519
|
+
tool: "*",
|
|
15520
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
15521
|
+
verdict,
|
|
15522
|
+
reason: why
|
|
15523
|
+
},
|
|
15524
|
+
{
|
|
15525
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
15526
|
+
tool: "*",
|
|
15527
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
15528
|
+
verdict,
|
|
15529
|
+
reason: why
|
|
15530
|
+
},
|
|
15531
|
+
{
|
|
15532
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
15533
|
+
tool: "*",
|
|
15534
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
15535
|
+
verdict,
|
|
15536
|
+
reason: why
|
|
15537
|
+
}
|
|
15538
|
+
];
|
|
15539
|
+
}
|
|
15540
|
+
var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
|
|
15541
|
+
function withAccessReading(def) {
|
|
15542
|
+
let changed = false;
|
|
15543
|
+
const smartRules = def.smartRules.map((r) => {
|
|
15544
|
+
const [c, ...rest] = r.conditions ?? [];
|
|
15545
|
+
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) {
|
|
15546
|
+
return r;
|
|
15547
|
+
}
|
|
15548
|
+
changed = true;
|
|
15549
|
+
return { ...r, conditions: [{ ...c, reading: "access" }] };
|
|
15550
|
+
});
|
|
15551
|
+
return changed ? { ...def, smartRules } : def;
|
|
15552
|
+
}
|
|
15553
|
+
|
|
15554
|
+
// src/shields.ts
|
|
14443
15555
|
init_dist();
|
|
14444
15556
|
var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
14445
15557
|
function loadUserShields() {
|
|
@@ -14466,7 +15578,7 @@ function loadUserShields() {
|
|
|
14466
15578
|
`);
|
|
14467
15579
|
continue;
|
|
14468
15580
|
}
|
|
14469
|
-
result[v.ok.name] = v.ok;
|
|
15581
|
+
result[v.ok.name] = withAccessReading(v.ok);
|
|
14470
15582
|
} catch (err) {
|
|
14471
15583
|
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
14472
15584
|
`);
|
|
@@ -14618,90 +15730,28 @@ var CLASS_B_COMMAND_CHECKS = /* @__PURE__ */ new Set([
|
|
|
14618
15730
|
"evalDynamic",
|
|
14619
15731
|
"pipeChainHigh"
|
|
14620
15732
|
]);
|
|
14621
|
-
function applyManagedCommandChecks(local, managed, locked) {
|
|
14622
|
-
const next = { ...local };
|
|
14623
|
-
for (const key of COMMAND_CHECK_KEYS) {
|
|
14624
|
-
const m = managed[key];
|
|
14625
|
-
if (typeof m !== "string") continue;
|
|
14626
|
-
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
14627
|
-
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
14628
|
-
locked: locked.includes(lockKey)
|
|
14629
|
-
});
|
|
14630
|
-
if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
|
|
14631
|
-
next[key] = resolved;
|
|
14632
|
-
}
|
|
14633
|
-
return next;
|
|
14634
|
-
}
|
|
14635
|
-
function applyManagedApprovers(local, managed) {
|
|
14636
|
-
return {
|
|
14637
|
-
...local,
|
|
14638
|
-
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
14639
|
-
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
14640
|
-
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
14641
|
-
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
14642
|
-
};
|
|
14643
|
-
}
|
|
14644
|
-
|
|
14645
|
-
// src/shields/build.ts
|
|
14646
|
-
init_dist();
|
|
14647
|
-
function escapeRegex(s) {
|
|
14648
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14649
|
-
}
|
|
14650
|
-
function slug(s) {
|
|
14651
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
14652
|
-
}
|
|
14653
|
-
var B = "[\\s/\\\\]";
|
|
14654
|
-
var SEP = "[/\\\\]";
|
|
14655
|
-
function pathToRegexFragment(rawPath) {
|
|
14656
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
14657
|
-
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
14658
|
-
if (segments2.length === 0) return "";
|
|
14659
|
-
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
14660
|
-
}
|
|
14661
|
-
function pathMatchesFragment(candidate, rawPath) {
|
|
14662
|
-
const value = pathToRegexFragment(rawPath);
|
|
14663
|
-
if (!value || !candidate) return false;
|
|
14664
|
-
const re = getCompiledRegex(value);
|
|
14665
|
-
if (!re) return false;
|
|
14666
|
-
return re.test(candidate);
|
|
14667
|
-
}
|
|
14668
|
-
function pathRules(rawPath, verdict, reason) {
|
|
14669
|
-
const value = pathToRegexFragment(rawPath);
|
|
14670
|
-
if (!value) return [];
|
|
14671
|
-
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
14672
|
-
const s = slug(rawPath);
|
|
14673
|
-
return [
|
|
14674
|
-
{
|
|
14675
|
-
name: `${verdict}-path-${s}-bash`,
|
|
14676
|
-
tool: "bash",
|
|
14677
|
-
conditions: [{ field: "command", op: "matches", value }],
|
|
14678
|
-
verdict,
|
|
14679
|
-
reason: why
|
|
14680
|
-
},
|
|
14681
|
-
// Keep the historical `-anytool` name for the file_path rule: the
|
|
14682
|
-
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
14683
|
-
{
|
|
14684
|
-
name: `${verdict}-path-${s}-anytool`,
|
|
14685
|
-
tool: "*",
|
|
14686
|
-
conditions: [{ field: "file_path", op: "matches", value }],
|
|
14687
|
-
verdict,
|
|
14688
|
-
reason: why
|
|
14689
|
-
},
|
|
14690
|
-
{
|
|
14691
|
-
name: `${verdict}-path-${s}-anytool-path`,
|
|
14692
|
-
tool: "*",
|
|
14693
|
-
conditions: [{ field: "path", op: "matches", value }],
|
|
14694
|
-
verdict,
|
|
14695
|
-
reason: why
|
|
14696
|
-
},
|
|
14697
|
-
{
|
|
14698
|
-
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
14699
|
-
tool: "*",
|
|
14700
|
-
conditions: [{ field: "pattern", op: "matches", value }],
|
|
14701
|
-
verdict,
|
|
14702
|
-
reason: why
|
|
14703
|
-
}
|
|
14704
|
-
];
|
|
15733
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
15734
|
+
const next = { ...local };
|
|
15735
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
15736
|
+
const m = managed[key];
|
|
15737
|
+
if (typeof m !== "string") continue;
|
|
15738
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
15739
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
15740
|
+
locked: locked.includes(lockKey)
|
|
15741
|
+
});
|
|
15742
|
+
if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
|
|
15743
|
+
next[key] = resolved;
|
|
15744
|
+
}
|
|
15745
|
+
return next;
|
|
15746
|
+
}
|
|
15747
|
+
function applyManagedApprovers(local, managed) {
|
|
15748
|
+
return {
|
|
15749
|
+
...local,
|
|
15750
|
+
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
15751
|
+
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
15752
|
+
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
15753
|
+
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
15754
|
+
};
|
|
14705
15755
|
}
|
|
14706
15756
|
|
|
14707
15757
|
// src/auth/trusted-hosts.ts
|
|
@@ -14762,6 +15812,7 @@ function isTrustedHost(host2) {
|
|
|
14762
15812
|
|
|
14763
15813
|
// src/config/index.ts
|
|
14764
15814
|
init_dist();
|
|
15815
|
+
init_dist();
|
|
14765
15816
|
|
|
14766
15817
|
// src/config/v2.ts
|
|
14767
15818
|
init_dist();
|
|
@@ -15074,7 +16125,9 @@ var DEFAULT_CONFIG = {
|
|
|
15074
16125
|
op: "matches",
|
|
15075
16126
|
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
15076
16127
|
// fails the engine's ReDoS check and the rule would be dropped.
|
|
15077
|
-
|
|
16128
|
+
// `apps`: turning an app off, allowing one of its actions, or
|
|
16129
|
+
// linking a server to an app (the general checks leave it).
|
|
16130
|
+
value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|apps (link|[a-z0-9-]{1,32} (off|[a-z]{1,32} allow))|egress allow|approver set)\\b",
|
|
15078
16131
|
flags: "i"
|
|
15079
16132
|
}
|
|
15080
16133
|
],
|
|
@@ -15160,7 +16213,12 @@ var DEFAULT_CONFIG = {
|
|
|
15160
16213
|
verdict: "block",
|
|
15161
16214
|
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
15162
16215
|
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
15163
|
-
}
|
|
16216
|
+
},
|
|
16217
|
+
// ── Actions in apps (node9Firewall#390) ───────────────────────────────
|
|
16218
|
+
// actions.money / actions.access / actions.publish: the action templates
|
|
16219
|
+
// on every MCP server no app shield claims, plus the shell publishing
|
|
16220
|
+
// rules. Review by default; the check value governs them.
|
|
16221
|
+
...GENERAL_ACTION_RULES
|
|
15164
16222
|
],
|
|
15165
16223
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
|
|
15166
16224
|
egress: {
|
|
@@ -15197,6 +16255,7 @@ var DEFAULT_CONFIG = {
|
|
|
15197
16255
|
trustedHosts: [],
|
|
15198
16256
|
trustedHostsManaged: false,
|
|
15199
16257
|
appPermissions: {},
|
|
16258
|
+
appLinks: {},
|
|
15200
16259
|
managedJailPaths: []
|
|
15201
16260
|
},
|
|
15202
16261
|
environments: {}
|
|
@@ -15431,6 +16490,7 @@ function getConfig(cwd) {
|
|
|
15431
16490
|
trustedHosts: [],
|
|
15432
16491
|
trustedHostsManaged: false,
|
|
15433
16492
|
appPermissions: {},
|
|
16493
|
+
appLinks: {},
|
|
15434
16494
|
managedJailPaths: []
|
|
15435
16495
|
};
|
|
15436
16496
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
@@ -15815,19 +16875,26 @@ function getConfig(cwd) {
|
|
|
15815
16875
|
}
|
|
15816
16876
|
if (Array.isArray(mc.jailPaths)) {
|
|
15817
16877
|
for (const jp of mc.jailPaths) {
|
|
15818
|
-
const
|
|
15819
|
-
if (!
|
|
16878
|
+
const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
16879
|
+
if (!path28) continue;
|
|
15820
16880
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
15821
|
-
for (const r of pathRules(
|
|
16881
|
+
for (const r of pathRules(path28, verdict, "org-managed jail")) {
|
|
15822
16882
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
15823
16883
|
}
|
|
15824
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
16884
|
+
mergedPolicy.managedJailPaths.push({ path: path28, verdict });
|
|
15825
16885
|
}
|
|
15826
16886
|
}
|
|
15827
16887
|
if (Array.isArray(mc.trustedHosts)) {
|
|
15828
16888
|
mergedPolicy.trustedHostsManaged = true;
|
|
15829
16889
|
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
15830
16890
|
}
|
|
16891
|
+
if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
|
|
16892
|
+
const links = {};
|
|
16893
|
+
for (const [key, slug2] of Object.entries(mc.appLinks)) {
|
|
16894
|
+
if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
|
|
16895
|
+
}
|
|
16896
|
+
mergedPolicy.appLinks = links;
|
|
16897
|
+
}
|
|
15831
16898
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
15832
16899
|
const coerced = {};
|
|
15833
16900
|
for (const [srv, tools] of Object.entries(mc.appPermissions)) {
|
|
@@ -16373,7 +17440,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
16373
17440
|
cwd: prepared.cwd,
|
|
16374
17441
|
activeEnvironment,
|
|
16375
17442
|
skipIgnoredFastPath: opts?.skipIgnoredFastPath,
|
|
16376
|
-
argumentScan: opts?.argumentScan
|
|
17443
|
+
argumentScan: opts?.argumentScan,
|
|
17444
|
+
mcpServer: opts?.mcpServer
|
|
16377
17445
|
},
|
|
16378
17446
|
{
|
|
16379
17447
|
checkProvenance,
|
|
@@ -17041,14 +18109,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
|
17041
18109
|
|
|
17042
18110
|
// src/auth/orchestrator.ts
|
|
17043
18111
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
17044
|
-
|
|
18112
|
+
|
|
18113
|
+
// src/apps/state.ts
|
|
18114
|
+
import fs12 from "fs";
|
|
18115
|
+
import os10 from "os";
|
|
18116
|
+
import path12 from "path";
|
|
18117
|
+
|
|
18118
|
+
// src/apps/catalog.ts
|
|
18119
|
+
var APP_CATALOG = [
|
|
18120
|
+
{
|
|
18121
|
+
slug: "redis",
|
|
18122
|
+
label: "Redis",
|
|
18123
|
+
shield: "redis",
|
|
18124
|
+
servers: ["redis", "valkey"],
|
|
18125
|
+
commands: ["redis-cli", "valkey-cli"],
|
|
18126
|
+
packages: ["redis", "ioredis", "valkey"],
|
|
18127
|
+
envPrefixes: ["REDIS_"]
|
|
18128
|
+
},
|
|
18129
|
+
{
|
|
18130
|
+
slug: "postgres",
|
|
18131
|
+
label: "PostgreSQL",
|
|
18132
|
+
shield: "postgres",
|
|
18133
|
+
servers: ["postgres", "pgsql"],
|
|
18134
|
+
commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
|
|
18135
|
+
packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
|
|
18136
|
+
envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
|
|
18137
|
+
},
|
|
18138
|
+
{
|
|
18139
|
+
slug: "mongodb",
|
|
18140
|
+
label: "MongoDB",
|
|
18141
|
+
shield: "mongodb",
|
|
18142
|
+
servers: ["mongo"],
|
|
18143
|
+
commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
|
|
18144
|
+
packages: ["mongodb", "mongoose", "pymongo", "motor"],
|
|
18145
|
+
envPrefixes: ["MONGO"]
|
|
18146
|
+
},
|
|
18147
|
+
{
|
|
18148
|
+
slug: "github",
|
|
18149
|
+
label: "GitHub",
|
|
18150
|
+
shield: "github",
|
|
18151
|
+
servers: ["github"],
|
|
18152
|
+
commands: ["gh"],
|
|
18153
|
+
packages: ["@octokit/", "octokit", "pygithub"],
|
|
18154
|
+
envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
|
|
18155
|
+
},
|
|
18156
|
+
{
|
|
18157
|
+
slug: "aws",
|
|
18158
|
+
label: "AWS",
|
|
18159
|
+
shield: "aws",
|
|
18160
|
+
servers: ["aws"],
|
|
18161
|
+
commands: ["aws", "sam", "cdk"],
|
|
18162
|
+
packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
|
|
18163
|
+
envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
|
|
18164
|
+
},
|
|
18165
|
+
{
|
|
18166
|
+
slug: "k8s",
|
|
18167
|
+
label: "Kubernetes",
|
|
18168
|
+
shield: "k8s",
|
|
18169
|
+
servers: ["kubernetes", "k8s", "kubectl"],
|
|
18170
|
+
commands: ["kubectl", "helm", "k9s"],
|
|
18171
|
+
packages: ["@kubernetes/client-node", "kubernetes"],
|
|
18172
|
+
envPrefixes: ["KUBECONFIG"]
|
|
18173
|
+
},
|
|
18174
|
+
{
|
|
18175
|
+
slug: "docker",
|
|
18176
|
+
label: "Docker",
|
|
18177
|
+
shield: "docker",
|
|
18178
|
+
servers: ["docker"],
|
|
18179
|
+
commands: ["docker", "docker-compose", "podman"],
|
|
18180
|
+
packages: ["dockerode", "docker"],
|
|
18181
|
+
envPrefixes: ["DOCKER_HOST"]
|
|
18182
|
+
},
|
|
18183
|
+
{
|
|
18184
|
+
slug: "stripe",
|
|
18185
|
+
label: "Stripe",
|
|
18186
|
+
shield: "stripe",
|
|
18187
|
+
servers: ["stripe"],
|
|
18188
|
+
commands: ["stripe"],
|
|
18189
|
+
packages: ["stripe", "@stripe/"],
|
|
18190
|
+
envPrefixes: ["STRIPE_"]
|
|
18191
|
+
},
|
|
18192
|
+
{
|
|
18193
|
+
slug: "paypal",
|
|
18194
|
+
label: "PayPal",
|
|
18195
|
+
shield: "paypal",
|
|
18196
|
+
servers: ["paypal"],
|
|
18197
|
+
commands: [],
|
|
18198
|
+
packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
|
|
18199
|
+
envPrefixes: ["PAYPAL_"]
|
|
18200
|
+
},
|
|
18201
|
+
{
|
|
18202
|
+
slug: "square",
|
|
18203
|
+
label: "Square",
|
|
18204
|
+
shield: "square",
|
|
18205
|
+
servers: ["square"],
|
|
18206
|
+
// Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
|
|
18207
|
+
notServers: ["squarespace"],
|
|
18208
|
+
commands: [],
|
|
18209
|
+
packages: ["square", "squareup"],
|
|
18210
|
+
envPrefixes: ["SQUARE_"]
|
|
18211
|
+
},
|
|
18212
|
+
{
|
|
18213
|
+
slug: "plaid",
|
|
18214
|
+
label: "Plaid",
|
|
18215
|
+
shield: "plaid",
|
|
18216
|
+
servers: ["plaid"],
|
|
18217
|
+
commands: [],
|
|
18218
|
+
packages: ["plaid", "plaid-python"],
|
|
18219
|
+
envPrefixes: ["PLAID_"]
|
|
18220
|
+
},
|
|
18221
|
+
{
|
|
18222
|
+
slug: "okta",
|
|
18223
|
+
label: "Okta",
|
|
18224
|
+
shield: "okta",
|
|
18225
|
+
servers: ["okta"],
|
|
18226
|
+
commands: ["okta"],
|
|
18227
|
+
packages: ["@okta/", "okta"],
|
|
18228
|
+
envPrefixes: ["OKTA_"]
|
|
18229
|
+
},
|
|
18230
|
+
{
|
|
18231
|
+
slug: "auth0",
|
|
18232
|
+
label: "Auth0",
|
|
18233
|
+
shield: "auth0",
|
|
18234
|
+
servers: ["auth0"],
|
|
18235
|
+
commands: ["auth0"],
|
|
18236
|
+
packages: ["auth0", "@auth0/"],
|
|
18237
|
+
envPrefixes: ["AUTH0_"]
|
|
18238
|
+
}
|
|
18239
|
+
];
|
|
18240
|
+
var APP_BY_SLUG = new Map(
|
|
18241
|
+
APP_CATALOG.map((a) => [a.slug, a])
|
|
18242
|
+
);
|
|
18243
|
+
|
|
18244
|
+
// src/apps/state.ts
|
|
18245
|
+
init_audit();
|
|
18246
|
+
|
|
18247
|
+
// src/daemon/mcp-tools.ts
|
|
18248
|
+
import fs11 from "fs";
|
|
18249
|
+
import path11 from "path";
|
|
18250
|
+
import os9 from "os";
|
|
18251
|
+
function getMcpToolsFile() {
|
|
18252
|
+
return path11.join(os9.homedir(), ".node9", "mcp-tools.json");
|
|
18253
|
+
}
|
|
18254
|
+
function readMcpToolsConfig() {
|
|
18255
|
+
try {
|
|
18256
|
+
const file2 = getMcpToolsFile();
|
|
18257
|
+
if (!fs11.existsSync(file2)) return {};
|
|
18258
|
+
const raw = fs11.readFileSync(file2, "utf-8");
|
|
18259
|
+
return JSON.parse(raw);
|
|
18260
|
+
} catch {
|
|
18261
|
+
return {};
|
|
18262
|
+
}
|
|
18263
|
+
}
|
|
18264
|
+
|
|
18265
|
+
// src/apps/state.ts
|
|
18266
|
+
function appsFilePath(home = os10.homedir()) {
|
|
18267
|
+
return path12.join(home, ".node9", "apps.json");
|
|
18268
|
+
}
|
|
18269
|
+
var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
|
|
18270
|
+
function emptyFile() {
|
|
18271
|
+
return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
|
|
18272
|
+
}
|
|
18273
|
+
function ownString(map, key) {
|
|
18274
|
+
if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
|
|
18275
|
+
const v = map[key];
|
|
18276
|
+
return typeof v === "string" ? v : void 0;
|
|
18277
|
+
}
|
|
18278
|
+
function readSignals(raw) {
|
|
18279
|
+
if (!Array.isArray(raw)) return [];
|
|
18280
|
+
const out = [];
|
|
18281
|
+
for (const x of raw) {
|
|
18282
|
+
if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
|
|
18283
|
+
if (!SIGNAL_KINDS.has(x.kind)) continue;
|
|
18284
|
+
out.push({
|
|
18285
|
+
kind: x.kind,
|
|
18286
|
+
value: x.value.slice(0, 256),
|
|
18287
|
+
...typeof x.project === "string" ? { project: x.project } : {}
|
|
18288
|
+
});
|
|
18289
|
+
}
|
|
18290
|
+
return out;
|
|
18291
|
+
}
|
|
18292
|
+
function parseAppsFile(text) {
|
|
18293
|
+
const raw = JSON.parse(text);
|
|
18294
|
+
const file2 = emptyFile();
|
|
18295
|
+
for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
|
|
18296
|
+
if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
|
|
18297
|
+
file2.apps[slug2] = {
|
|
18298
|
+
state: r.state,
|
|
18299
|
+
by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
|
|
18300
|
+
at: typeof r.at === "string" ? r.at : "",
|
|
18301
|
+
signals: readSignals(r.signals)
|
|
18302
|
+
};
|
|
18303
|
+
}
|
|
18304
|
+
for (const [server, slug2] of Object.entries(raw.links ?? {})) {
|
|
18305
|
+
if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
|
|
18306
|
+
}
|
|
18307
|
+
return file2;
|
|
18308
|
+
}
|
|
18309
|
+
function readAppsFile(home) {
|
|
18310
|
+
return readAppsFileStrict(home) ?? emptyFile();
|
|
18311
|
+
}
|
|
18312
|
+
function readAppsFileStrict(home) {
|
|
18313
|
+
let text;
|
|
18314
|
+
try {
|
|
18315
|
+
text = fs12.readFileSync(appsFilePath(home), "utf8");
|
|
18316
|
+
} catch (e) {
|
|
18317
|
+
return e.code === "ENOENT" ? emptyFile() : null;
|
|
18318
|
+
}
|
|
18319
|
+
try {
|
|
18320
|
+
return parseAppsFile(text);
|
|
18321
|
+
} catch {
|
|
18322
|
+
return null;
|
|
18323
|
+
}
|
|
18324
|
+
}
|
|
18325
|
+
var linkCache = null;
|
|
18326
|
+
var NO_LINKS = /* @__PURE__ */ Object.create(null);
|
|
18327
|
+
function localLinks() {
|
|
18328
|
+
const file2 = appsFilePath();
|
|
18329
|
+
try {
|
|
18330
|
+
const mtimeMs = fs12.statSync(file2).mtimeMs;
|
|
18331
|
+
if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
|
|
18332
|
+
linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
|
|
18333
|
+
}
|
|
18334
|
+
return linkCache.links;
|
|
18335
|
+
} catch {
|
|
18336
|
+
return NO_LINKS;
|
|
18337
|
+
}
|
|
18338
|
+
}
|
|
18339
|
+
function resolveLinkedServer(label2, opts = {}) {
|
|
18340
|
+
if (typeof label2 !== "string" || !label2) return label2;
|
|
18341
|
+
const applied = new Set(opts.appliedShields ?? []);
|
|
18342
|
+
const live = (slug2) => {
|
|
18343
|
+
const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
|
|
18344
|
+
return app && applied.has(app.shield) ? slug2 : void 0;
|
|
18345
|
+
};
|
|
18346
|
+
const lower2 = label2.toLowerCase();
|
|
18347
|
+
if (!opts.workspaceGoverned) {
|
|
18348
|
+
const local = live(ownString(localLinks(), lower2));
|
|
18349
|
+
if (local) return local;
|
|
18350
|
+
}
|
|
18351
|
+
const managed = opts.managedLinks ?? {};
|
|
18352
|
+
if (opts.serverKey) {
|
|
18353
|
+
const byKey = live(ownString(managed, opts.serverKey));
|
|
18354
|
+
if (byKey) return byKey;
|
|
18355
|
+
}
|
|
18356
|
+
const keys = Object.keys(managed);
|
|
18357
|
+
if (keys.length === 0) return label2;
|
|
18358
|
+
const tools = readMcpToolsConfig();
|
|
18359
|
+
for (const key of keys) {
|
|
18360
|
+
const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
|
|
18361
|
+
if (typeof name === "string" && name.toLowerCase() === lower2) {
|
|
18362
|
+
const linked = live(ownString(managed, key));
|
|
18363
|
+
if (linked) return linked;
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
return label2;
|
|
18367
|
+
}
|
|
18368
|
+
|
|
18369
|
+
// src/auth/orchestrator.ts
|
|
18370
|
+
import os20 from "os";
|
|
17045
18371
|
|
|
17046
18372
|
// src/ui/native.ts
|
|
17047
18373
|
import { spawn } from "child_process";
|
|
17048
|
-
import
|
|
18374
|
+
import path14 from "path";
|
|
17049
18375
|
|
|
17050
18376
|
// src/context-sniper.ts
|
|
17051
|
-
import
|
|
18377
|
+
import path13 from "path";
|
|
17052
18378
|
function smartTruncate(str, maxLen = 500) {
|
|
17053
18379
|
if (str.length <= maxLen) return str;
|
|
17054
18380
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -17116,7 +18442,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
17116
18442
|
intent = "EDIT";
|
|
17117
18443
|
if (obj.file_path) {
|
|
17118
18444
|
editFilePath = String(obj.file_path);
|
|
17119
|
-
editFileName =
|
|
18445
|
+
editFileName = path13.basename(editFilePath);
|
|
17120
18446
|
}
|
|
17121
18447
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
17122
18448
|
contextSnippet = result.snippet;
|
|
@@ -17184,7 +18510,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
17184
18510
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17185
18511
|
const obj = parsed;
|
|
17186
18512
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
17187
|
-
const file2 = obj.file_path ?
|
|
18513
|
+
const file2 = obj.file_path ? path14.basename(String(obj.file_path)) : "file";
|
|
17188
18514
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
17189
18515
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
17190
18516
|
return {
|
|
@@ -17387,24 +18713,24 @@ end run`;
|
|
|
17387
18713
|
}
|
|
17388
18714
|
|
|
17389
18715
|
// src/canary/registry.ts
|
|
17390
|
-
import
|
|
17391
|
-
import
|
|
17392
|
-
import
|
|
18716
|
+
import fs14 from "fs";
|
|
18717
|
+
import os12 from "os";
|
|
18718
|
+
import path16 from "path";
|
|
17393
18719
|
init_dist();
|
|
17394
18720
|
|
|
17395
18721
|
// src/shields/jail.ts
|
|
17396
|
-
import
|
|
17397
|
-
import
|
|
17398
|
-
import
|
|
18722
|
+
import fs13 from "fs";
|
|
18723
|
+
import os11 from "os";
|
|
18724
|
+
import path15 from "path";
|
|
17399
18725
|
init_dist();
|
|
17400
18726
|
var USER_JAIL_SHIELD = "user-jail";
|
|
17401
18727
|
function jailStorePath() {
|
|
17402
|
-
return
|
|
18728
|
+
return path15.join(os11.homedir(), ".node9", "jail-paths.json");
|
|
17403
18729
|
}
|
|
17404
18730
|
function readJailPaths() {
|
|
17405
18731
|
let text;
|
|
17406
18732
|
try {
|
|
17407
|
-
text =
|
|
18733
|
+
text = fs13.readFileSync(jailStorePath(), "utf8");
|
|
17408
18734
|
} catch (err) {
|
|
17409
18735
|
if (err.code === "ENOENT") return [];
|
|
17410
18736
|
throw err;
|
|
@@ -17433,7 +18759,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
17433
18759
|
|
|
17434
18760
|
// src/canary/registry.ts
|
|
17435
18761
|
function canaryStorePath() {
|
|
17436
|
-
return
|
|
18762
|
+
return path16.join(os12.homedir(), ".node9", "canaries.json");
|
|
17437
18763
|
}
|
|
17438
18764
|
function isRecord(x) {
|
|
17439
18765
|
if (!x || typeof x !== "object") return false;
|
|
@@ -17444,7 +18770,7 @@ function loadCanaries(opts) {
|
|
|
17444
18770
|
const p = canaryStorePath();
|
|
17445
18771
|
let raw;
|
|
17446
18772
|
try {
|
|
17447
|
-
raw =
|
|
18773
|
+
raw = fs14.readFileSync(p, "utf-8");
|
|
17448
18774
|
} catch (e) {
|
|
17449
18775
|
if (e.code === "ENOENT") return [];
|
|
17450
18776
|
throw e;
|
|
@@ -17471,21 +18797,21 @@ init_dist();
|
|
|
17471
18797
|
|
|
17472
18798
|
// src/auth/session-script.ts
|
|
17473
18799
|
init_dist();
|
|
17474
|
-
import
|
|
17475
|
-
import
|
|
17476
|
-
import
|
|
18800
|
+
import fs15 from "fs";
|
|
18801
|
+
import os13 from "os";
|
|
18802
|
+
import path17 from "path";
|
|
17477
18803
|
var MAX_SCRIPT_BYTES = 64 * 1024;
|
|
17478
18804
|
var MAX_RECORDED_WRITES = 32;
|
|
17479
18805
|
function readScriptFile(resolved) {
|
|
17480
18806
|
let fd;
|
|
17481
18807
|
try {
|
|
17482
|
-
fd =
|
|
17483
|
-
const st =
|
|
18808
|
+
fd = fs15.openSync(resolved, fs15.constants.O_RDONLY | (fs15.constants.O_NONBLOCK ?? 0));
|
|
18809
|
+
const st = fs15.fstatSync(fd);
|
|
17484
18810
|
if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
|
|
17485
18811
|
const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
|
|
17486
18812
|
let n = 0;
|
|
17487
18813
|
while (n < buf.length) {
|
|
17488
|
-
const got =
|
|
18814
|
+
const got = fs15.readSync(fd, buf, n, buf.length - n, n);
|
|
17489
18815
|
if (got === 0) break;
|
|
17490
18816
|
n += got;
|
|
17491
18817
|
}
|
|
@@ -17496,7 +18822,7 @@ function readScriptFile(resolved) {
|
|
|
17496
18822
|
} finally {
|
|
17497
18823
|
if (fd !== void 0) {
|
|
17498
18824
|
try {
|
|
17499
|
-
|
|
18825
|
+
fs15.closeSync(fd);
|
|
17500
18826
|
} catch {
|
|
17501
18827
|
}
|
|
17502
18828
|
}
|
|
@@ -17504,9 +18830,9 @@ function readScriptFile(resolved) {
|
|
|
17504
18830
|
}
|
|
17505
18831
|
function absolute(p, cwd, home) {
|
|
17506
18832
|
const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
|
|
17507
|
-
return
|
|
18833
|
+
return path17.resolve(cwd, spelled);
|
|
17508
18834
|
}
|
|
17509
|
-
async function resolveSessionScript(toolName, args, cwd, layers, deps, home =
|
|
18835
|
+
async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os13.homedir()) {
|
|
17510
18836
|
try {
|
|
17511
18837
|
const d = deps ?? { checkWritten, readScript: readScriptFile };
|
|
17512
18838
|
const shell = resolveShellInput(toolName, args, layers);
|
|
@@ -17535,7 +18861,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os
|
|
|
17535
18861
|
return null;
|
|
17536
18862
|
}
|
|
17537
18863
|
}
|
|
17538
|
-
function sessionWriteTargets(toolName, args, fileAccess, cwd, home =
|
|
18864
|
+
function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os13.homedir()) {
|
|
17539
18865
|
const base2 = cwd ?? process.cwd();
|
|
17540
18866
|
const out = /* @__PURE__ */ new Set();
|
|
17541
18867
|
const argsObj = args && typeof args === "object" ? args : {};
|
|
@@ -17565,26 +18891,26 @@ init_audit();
|
|
|
17565
18891
|
init_dist();
|
|
17566
18892
|
|
|
17567
18893
|
// src/exposure/known-hosts.ts
|
|
17568
|
-
import
|
|
17569
|
-
import
|
|
18894
|
+
import fs17 from "fs";
|
|
18895
|
+
import path19 from "path";
|
|
17570
18896
|
|
|
17571
18897
|
// src/utils/git-remotes.ts
|
|
17572
18898
|
init_dist();
|
|
17573
|
-
import
|
|
17574
|
-
import
|
|
18899
|
+
import fs16 from "fs";
|
|
18900
|
+
import path18 from "path";
|
|
17575
18901
|
function readSmallFile(p, max) {
|
|
17576
18902
|
let fd;
|
|
17577
18903
|
try {
|
|
17578
|
-
fd =
|
|
17579
|
-
const st =
|
|
18904
|
+
fd = fs16.openSync(p, fs16.constants.O_RDONLY | (fs16.constants.O_NOFOLLOW ?? 0));
|
|
18905
|
+
const st = fs16.fstatSync(fd);
|
|
17580
18906
|
if (!st.isFile() || st.size > max) return null;
|
|
17581
18907
|
const buf = Buffer.alloc(st.size);
|
|
17582
|
-
const n =
|
|
18908
|
+
const n = fs16.readSync(fd, buf, 0, st.size, 0);
|
|
17583
18909
|
return buf.subarray(0, n).toString("utf8");
|
|
17584
18910
|
} catch {
|
|
17585
18911
|
return null;
|
|
17586
18912
|
} finally {
|
|
17587
|
-
if (fd !== void 0)
|
|
18913
|
+
if (fd !== void 0) fs16.closeSync(fd);
|
|
17588
18914
|
}
|
|
17589
18915
|
}
|
|
17590
18916
|
function gitRemoteHost(url) {
|
|
@@ -17603,20 +18929,20 @@ function gitRemoteHost(url) {
|
|
|
17603
18929
|
return null;
|
|
17604
18930
|
}
|
|
17605
18931
|
function findGitConfig(dir2, walkUp = true) {
|
|
17606
|
-
let cur =
|
|
18932
|
+
let cur = path18.resolve(dir2);
|
|
17607
18933
|
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
17608
|
-
const dotGit =
|
|
17609
|
-
const direct = readSmallFile(
|
|
18934
|
+
const dotGit = path18.join(cur, ".git");
|
|
18935
|
+
const direct = readSmallFile(path18.join(dotGit, "config"), 1e6);
|
|
17610
18936
|
if (direct !== null) return direct;
|
|
17611
18937
|
const pointer = readSmallFile(dotGit, 4096);
|
|
17612
18938
|
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
17613
18939
|
if (m) {
|
|
17614
|
-
let gitDir =
|
|
17615
|
-
const common = readSmallFile(
|
|
17616
|
-
if (common !== null) gitDir =
|
|
17617
|
-
return readSmallFile(
|
|
18940
|
+
let gitDir = path18.resolve(cur, m[1]);
|
|
18941
|
+
const common = readSmallFile(path18.join(gitDir, "commondir"), 4096);
|
|
18942
|
+
if (common !== null) gitDir = path18.resolve(gitDir, common.trim());
|
|
18943
|
+
return readSmallFile(path18.join(gitDir, "config"), 1e6);
|
|
17618
18944
|
}
|
|
17619
|
-
const parent =
|
|
18945
|
+
const parent = path18.dirname(cur);
|
|
17620
18946
|
if (parent === cur) return null;
|
|
17621
18947
|
cur = parent;
|
|
17622
18948
|
}
|
|
@@ -17626,12 +18952,12 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
17626
18952
|
function readGitRemotes(dir2, opts = {}) {
|
|
17627
18953
|
if (!dir2) return [];
|
|
17628
18954
|
const walkUp = opts.walkUp !== false;
|
|
17629
|
-
const key = `${walkUp ? "up" : "at"}:${
|
|
18955
|
+
const key = `${walkUp ? "up" : "at"}:${path18.resolve(dir2)}`;
|
|
17630
18956
|
const hit = cache.get(key);
|
|
17631
18957
|
if (hit) return hit;
|
|
17632
18958
|
const out = [];
|
|
17633
18959
|
try {
|
|
17634
|
-
const cfg = findGitConfig(
|
|
18960
|
+
const cfg = findGitConfig(path18.resolve(dir2), walkUp);
|
|
17635
18961
|
if (cfg !== null) {
|
|
17636
18962
|
let current = null;
|
|
17637
18963
|
for (const line of cfg.split(/\r?\n/)) {
|
|
@@ -17667,15 +18993,15 @@ var ECOSYSTEMS = [
|
|
|
17667
18993
|
];
|
|
17668
18994
|
var cache2 = /* @__PURE__ */ new Map();
|
|
17669
18995
|
function manifestRoot(dir2) {
|
|
17670
|
-
let cur =
|
|
18996
|
+
let cur = path19.resolve(dir2);
|
|
17671
18997
|
for (let i = 0; i < 64; i++) {
|
|
17672
18998
|
for (const [m] of ECOSYSTEMS) {
|
|
17673
18999
|
try {
|
|
17674
|
-
if (
|
|
19000
|
+
if (fs17.existsSync(path19.join(cur, m))) return cur;
|
|
17675
19001
|
} catch {
|
|
17676
19002
|
}
|
|
17677
19003
|
}
|
|
17678
|
-
const parent =
|
|
19004
|
+
const parent = path19.dirname(cur);
|
|
17679
19005
|
if (parent === cur) return null;
|
|
17680
19006
|
cur = parent;
|
|
17681
19007
|
}
|
|
@@ -17684,7 +19010,7 @@ function manifestRoot(dir2) {
|
|
|
17684
19010
|
function knownProjectHosts(dir2) {
|
|
17685
19011
|
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
17686
19012
|
if (!dir2) return empty;
|
|
17687
|
-
const key =
|
|
19013
|
+
const key = path19.resolve(dir2);
|
|
17688
19014
|
const hit = cache2.get(key);
|
|
17689
19015
|
if (hit) return hit;
|
|
17690
19016
|
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
@@ -17697,10 +19023,10 @@ function knownProjectHosts(dir2) {
|
|
|
17697
19023
|
const root = manifestRoot(key);
|
|
17698
19024
|
if (root !== null) {
|
|
17699
19025
|
for (const [m, registry] of ECOSYSTEMS) {
|
|
17700
|
-
if (
|
|
19026
|
+
if (fs17.existsSync(path19.join(root, m))) out.hosts.add(registry);
|
|
17701
19027
|
}
|
|
17702
19028
|
try {
|
|
17703
|
-
if (
|
|
19029
|
+
if (fs17.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
17704
19030
|
out.hosts.add("host:rubygems.org");
|
|
17705
19031
|
} catch {
|
|
17706
19032
|
}
|
|
@@ -17713,9 +19039,9 @@ function knownProjectHosts(dir2) {
|
|
|
17713
19039
|
|
|
17714
19040
|
// src/exposure/writes.ts
|
|
17715
19041
|
init_dist();
|
|
17716
|
-
import
|
|
17717
|
-
import
|
|
17718
|
-
import
|
|
19042
|
+
import fs18 from "fs";
|
|
19043
|
+
import os14 from "os";
|
|
19044
|
+
import path20 from "path";
|
|
17719
19045
|
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
17720
19046
|
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
17721
19047
|
function writtenContent(args) {
|
|
@@ -17744,7 +19070,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17744
19070
|
if (typeof a.content === "string") {
|
|
17745
19071
|
try {
|
|
17746
19072
|
const next = JSON.parse(a.content);
|
|
17747
|
-
const prev = JSON.parse(
|
|
19073
|
+
const prev = JSON.parse(fs18.readFileSync(absPath, "utf8"));
|
|
17748
19074
|
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
17749
19075
|
} catch {
|
|
17750
19076
|
return LIFECYCLE_RE.test(a.content);
|
|
@@ -17754,8 +19080,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
17754
19080
|
}
|
|
17755
19081
|
function judgeWritesAfterExposure(opts) {
|
|
17756
19082
|
const holds = [];
|
|
17757
|
-
const home = opts.home ??
|
|
17758
|
-
const tmp = normalizeFilePath(
|
|
19083
|
+
const home = opts.home ?? os14.homedir();
|
|
19084
|
+
const tmp = normalizeFilePath(os14.tmpdir());
|
|
17759
19085
|
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
17760
19086
|
const seen = /* @__PURE__ */ new Set();
|
|
17761
19087
|
for (const access of opts.accesses) {
|
|
@@ -17765,7 +19091,7 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17765
19091
|
seen.add(p);
|
|
17766
19092
|
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
17767
19093
|
if (root !== null && isInside(root, p)) {
|
|
17768
|
-
if (isAgentInstructionPath(p, root) && (
|
|
19094
|
+
if (isAgentInstructionPath(p, root) && (path20.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
17769
19095
|
holds.push({ path: p, why: "agent-instruction" });
|
|
17770
19096
|
continue;
|
|
17771
19097
|
}
|
|
@@ -17787,25 +19113,25 @@ function judgeWritesAfterExposure(opts) {
|
|
|
17787
19113
|
|
|
17788
19114
|
// src/exposure/marker.ts
|
|
17789
19115
|
import crypto3 from "crypto";
|
|
17790
|
-
import
|
|
17791
|
-
import
|
|
17792
|
-
import
|
|
19116
|
+
import fs19 from "fs";
|
|
19117
|
+
import os15 from "os";
|
|
19118
|
+
import path21 from "path";
|
|
17793
19119
|
|
|
17794
19120
|
// src/daemon/exposure-store.ts
|
|
17795
19121
|
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
17796
19122
|
|
|
17797
19123
|
// src/exposure/marker.ts
|
|
17798
19124
|
function dir() {
|
|
17799
|
-
return
|
|
19125
|
+
return path21.join(os15.homedir(), ".node9", "exposure-sessions");
|
|
17800
19126
|
}
|
|
17801
19127
|
function file(sessionId) {
|
|
17802
19128
|
const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
17803
|
-
return
|
|
19129
|
+
return path21.join(dir(), id);
|
|
17804
19130
|
}
|
|
17805
19131
|
function maybeSessionExposed(sessionId) {
|
|
17806
19132
|
if (!sessionId) return false;
|
|
17807
19133
|
try {
|
|
17808
|
-
const st =
|
|
19134
|
+
const st = fs19.statSync(file(sessionId));
|
|
17809
19135
|
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
17810
19136
|
} catch (e) {
|
|
17811
19137
|
return e.code !== "ENOENT";
|
|
@@ -17813,27 +19139,7 @@ function maybeSessionExposed(sessionId) {
|
|
|
17813
19139
|
}
|
|
17814
19140
|
|
|
17815
19141
|
// src/exposure/roles.ts
|
|
17816
|
-
import
|
|
17817
|
-
|
|
17818
|
-
// src/daemon/mcp-tools.ts
|
|
17819
|
-
import fs18 from "fs";
|
|
17820
|
-
import path20 from "path";
|
|
17821
|
-
import os14 from "os";
|
|
17822
|
-
function getMcpToolsFile() {
|
|
17823
|
-
return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
|
|
17824
|
-
}
|
|
17825
|
-
function readMcpToolsConfig() {
|
|
17826
|
-
try {
|
|
17827
|
-
const file2 = getMcpToolsFile();
|
|
17828
|
-
if (!fs18.existsSync(file2)) return {};
|
|
17829
|
-
const raw = fs18.readFileSync(file2, "utf-8");
|
|
17830
|
-
return JSON.parse(raw);
|
|
17831
|
-
} catch {
|
|
17832
|
-
return {};
|
|
17833
|
-
}
|
|
17834
|
-
}
|
|
17835
|
-
|
|
17836
|
-
// src/exposure/roles.ts
|
|
19142
|
+
import fs20 from "fs";
|
|
17837
19143
|
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
17838
19144
|
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
17839
19145
|
function foldServerName(name) {
|
|
@@ -17843,7 +19149,7 @@ var inventoryCache = null;
|
|
|
17843
19149
|
var inventoryStamp = null;
|
|
17844
19150
|
function inventoryFileStamp() {
|
|
17845
19151
|
try {
|
|
17846
|
-
const st =
|
|
19152
|
+
const st = fs20.statSync(getMcpToolsFile());
|
|
17847
19153
|
return `${st.mtimeMs}:${st.size}`;
|
|
17848
19154
|
} catch {
|
|
17849
19155
|
return "";
|
|
@@ -17928,15 +19234,15 @@ function sourceWord(toolName) {
|
|
|
17928
19234
|
|
|
17929
19235
|
// src/auth/trusted-recipients.ts
|
|
17930
19236
|
init_dist();
|
|
17931
|
-
import
|
|
17932
|
-
import
|
|
17933
|
-
import
|
|
19237
|
+
import fs21 from "fs";
|
|
19238
|
+
import path22 from "path";
|
|
19239
|
+
import os16 from "os";
|
|
17934
19240
|
function getTrustedRecipientsPath() {
|
|
17935
|
-
return
|
|
19241
|
+
return path22.join(os16.homedir(), ".node9", "trusted-recipients.json");
|
|
17936
19242
|
}
|
|
17937
19243
|
function readTrustedRecipients() {
|
|
17938
19244
|
try {
|
|
17939
|
-
const raw =
|
|
19245
|
+
const raw = fs21.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
17940
19246
|
const parsed = JSON.parse(raw);
|
|
17941
19247
|
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
17942
19248
|
} catch {
|
|
@@ -17945,10 +19251,10 @@ function readTrustedRecipients() {
|
|
|
17945
19251
|
}
|
|
17946
19252
|
function writeTrustedRecipients(entries) {
|
|
17947
19253
|
const filePath = getTrustedRecipientsPath();
|
|
17948
|
-
|
|
19254
|
+
fs21.mkdirSync(path22.dirname(filePath), { recursive: true });
|
|
17949
19255
|
const tmp = filePath + ".node9-tmp";
|
|
17950
|
-
|
|
17951
|
-
|
|
19256
|
+
fs21.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
19257
|
+
fs21.renameSync(tmp, filePath);
|
|
17952
19258
|
}
|
|
17953
19259
|
function addTrustedRecipient(raw, addedBy = "user") {
|
|
17954
19260
|
const value = normalizeRecipient(raw);
|
|
@@ -17963,7 +19269,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
|
|
|
17963
19269
|
var cache3 = null;
|
|
17964
19270
|
function mtimeOf() {
|
|
17965
19271
|
try {
|
|
17966
|
-
return
|
|
19272
|
+
return fs21.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
17967
19273
|
} catch {
|
|
17968
19274
|
return 0;
|
|
17969
19275
|
}
|
|
@@ -17978,40 +19284,40 @@ function isTrustedRecipient(value) {
|
|
|
17978
19284
|
|
|
17979
19285
|
// src/auth/cloud.ts
|
|
17980
19286
|
init_audit();
|
|
17981
|
-
import
|
|
17982
|
-
import
|
|
17983
|
-
import
|
|
19287
|
+
import fs23 from "fs";
|
|
19288
|
+
import os17 from "os";
|
|
19289
|
+
import path23 from "path";
|
|
17984
19290
|
|
|
17985
19291
|
// src/utils/read-capped.ts
|
|
17986
|
-
import
|
|
19292
|
+
import fs22 from "fs";
|
|
17987
19293
|
function readCapped(file2, maxBytes) {
|
|
17988
19294
|
if (maxBytes < 0) return null;
|
|
17989
19295
|
let fd;
|
|
17990
19296
|
try {
|
|
17991
|
-
fd =
|
|
19297
|
+
fd = fs22.openSync(file2, "r");
|
|
17992
19298
|
} catch {
|
|
17993
19299
|
return null;
|
|
17994
19300
|
}
|
|
17995
19301
|
try {
|
|
17996
|
-
const st =
|
|
19302
|
+
const st = fs22.fstatSync(fd);
|
|
17997
19303
|
if (!st.isFile()) return null;
|
|
17998
19304
|
const want = Math.min(st.size, maxBytes + 1);
|
|
17999
19305
|
const buf = Buffer.alloc(want);
|
|
18000
|
-
let
|
|
18001
|
-
while (
|
|
18002
|
-
const n =
|
|
19306
|
+
let read2 = 0;
|
|
19307
|
+
while (read2 < want) {
|
|
19308
|
+
const n = fs22.readSync(fd, buf, read2, want - read2, read2);
|
|
18003
19309
|
if (n <= 0) break;
|
|
18004
|
-
|
|
19310
|
+
read2 += n;
|
|
18005
19311
|
}
|
|
18006
19312
|
return {
|
|
18007
|
-
bytes: buf.subarray(0, Math.min(
|
|
18008
|
-
truncated:
|
|
19313
|
+
bytes: buf.subarray(0, Math.min(read2, maxBytes)),
|
|
19314
|
+
truncated: read2 > maxBytes
|
|
18009
19315
|
};
|
|
18010
19316
|
} catch {
|
|
18011
19317
|
return null;
|
|
18012
19318
|
} finally {
|
|
18013
19319
|
try {
|
|
18014
|
-
|
|
19320
|
+
fs22.closeSync(fd);
|
|
18015
19321
|
} catch {
|
|
18016
19322
|
}
|
|
18017
19323
|
}
|
|
@@ -18056,7 +19362,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18056
19362
|
let ciContext;
|
|
18057
19363
|
if (process.env.CI) {
|
|
18058
19364
|
try {
|
|
18059
|
-
const ciContextPath =
|
|
19365
|
+
const ciContextPath = path23.join(os17.homedir(), ".node9", "ci-context.json");
|
|
18060
19366
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
18061
19367
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
18062
19368
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -18098,9 +19404,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
18098
19404
|
context: {
|
|
18099
19405
|
agent: meta?.agent,
|
|
18100
19406
|
mcpServer: meta?.mcpServer,
|
|
18101
|
-
hostname:
|
|
19407
|
+
hostname: os17.hostname(),
|
|
18102
19408
|
cwd: process.cwd(),
|
|
18103
|
-
platform:
|
|
19409
|
+
platform: os17.platform()
|
|
18104
19410
|
},
|
|
18105
19411
|
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
18106
19412
|
// an unknown key, so only fields its schema declares go on the wire.
|
|
@@ -18174,14 +19480,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18174
19480
|
});
|
|
18175
19481
|
clearTimeout(timer);
|
|
18176
19482
|
if (!res.ok) {
|
|
18177
|
-
|
|
19483
|
+
fs23.appendFileSync(
|
|
18178
19484
|
HOOK_DEBUG_LOG,
|
|
18179
19485
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
18180
19486
|
`
|
|
18181
19487
|
);
|
|
18182
19488
|
}
|
|
18183
19489
|
} catch (err) {
|
|
18184
|
-
|
|
19490
|
+
fs23.appendFileSync(
|
|
18185
19491
|
HOOK_DEBUG_LOG,
|
|
18186
19492
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
18187
19493
|
`
|
|
@@ -18192,16 +19498,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
18192
19498
|
// src/loop-detector.ts
|
|
18193
19499
|
init_dist();
|
|
18194
19500
|
init_dist();
|
|
18195
|
-
import
|
|
18196
|
-
import
|
|
18197
|
-
import
|
|
19501
|
+
import fs24 from "fs";
|
|
19502
|
+
import path24 from "path";
|
|
19503
|
+
import os18 from "os";
|
|
18198
19504
|
function loopStateFile() {
|
|
18199
|
-
return
|
|
19505
|
+
return path24.join(os18.homedir(), ".node9", "loop-state.json");
|
|
18200
19506
|
}
|
|
18201
19507
|
function readState() {
|
|
18202
19508
|
try {
|
|
18203
|
-
if (!
|
|
18204
|
-
const raw =
|
|
19509
|
+
if (!fs24.existsSync(loopStateFile())) return [];
|
|
19510
|
+
const raw = fs24.readFileSync(loopStateFile(), "utf-8");
|
|
18205
19511
|
const parsed = JSON.parse(raw);
|
|
18206
19512
|
if (!Array.isArray(parsed)) return [];
|
|
18207
19513
|
return parsed;
|
|
@@ -18210,11 +19516,11 @@ function readState() {
|
|
|
18210
19516
|
}
|
|
18211
19517
|
}
|
|
18212
19518
|
function writeState(records) {
|
|
18213
|
-
const dir2 =
|
|
18214
|
-
if (!
|
|
18215
|
-
const tmpPath = `${loopStateFile()}.${
|
|
18216
|
-
|
|
18217
|
-
|
|
19519
|
+
const dir2 = path24.dirname(loopStateFile());
|
|
19520
|
+
if (!fs24.existsSync(dir2)) fs24.mkdirSync(dir2, { recursive: true });
|
|
19521
|
+
const tmpPath = `${loopStateFile()}.${os18.hostname()}.${process.pid}.tmp`;
|
|
19522
|
+
fs24.writeFileSync(tmpPath, JSON.stringify(records));
|
|
19523
|
+
fs24.renameSync(tmpPath, loopStateFile());
|
|
18218
19524
|
}
|
|
18219
19525
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
18220
19526
|
try {
|
|
@@ -18229,20 +19535,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
18229
19535
|
// src/supply-chain/check.ts
|
|
18230
19536
|
init_dist();
|
|
18231
19537
|
import picomatch2 from "picomatch";
|
|
18232
|
-
import
|
|
19538
|
+
import path27 from "path";
|
|
18233
19539
|
|
|
18234
19540
|
// src/supply-chain/osv-index.ts
|
|
18235
19541
|
init_dist();
|
|
18236
|
-
import
|
|
18237
|
-
import
|
|
18238
|
-
import
|
|
19542
|
+
import fs25 from "fs";
|
|
19543
|
+
import os19 from "os";
|
|
19544
|
+
import path25 from "path";
|
|
18239
19545
|
import { createHash as createHash4 } from "crypto";
|
|
18240
19546
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
18241
19547
|
function osvRoot() {
|
|
18242
|
-
return
|
|
19548
|
+
return path25.join(os19.homedir(), ".node9", "osv");
|
|
18243
19549
|
}
|
|
18244
19550
|
function ecoDir(eco) {
|
|
18245
|
-
return
|
|
19551
|
+
return path25.join(osvRoot(), eco);
|
|
18246
19552
|
}
|
|
18247
19553
|
function indexKey(eco, name) {
|
|
18248
19554
|
switch (eco) {
|
|
@@ -18261,11 +19567,11 @@ function shardOf(key) {
|
|
|
18261
19567
|
return createHash4("sha1").update(key).digest("hex").slice(0, 2);
|
|
18262
19568
|
}
|
|
18263
19569
|
function shardPath(eco, shard) {
|
|
18264
|
-
return
|
|
19570
|
+
return path25.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
18265
19571
|
}
|
|
18266
19572
|
function readMeta(eco) {
|
|
18267
19573
|
try {
|
|
18268
|
-
const m = JSON.parse(
|
|
19574
|
+
const m = JSON.parse(fs25.readFileSync(path25.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
18269
19575
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
18270
19576
|
return m;
|
|
18271
19577
|
} catch {
|
|
@@ -18274,7 +19580,7 @@ function readMeta(eco) {
|
|
|
18274
19580
|
}
|
|
18275
19581
|
function readShard(eco, shard) {
|
|
18276
19582
|
try {
|
|
18277
|
-
const s = JSON.parse(
|
|
19583
|
+
const s = JSON.parse(fs25.readFileSync(shardPath(eco, shard), "utf8"));
|
|
18278
19584
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
18279
19585
|
} catch {
|
|
18280
19586
|
return {};
|
|
@@ -18401,13 +19707,101 @@ async function queryOsvMalicious(queries) {
|
|
|
18401
19707
|
|
|
18402
19708
|
// src/supply-chain/registry.ts
|
|
18403
19709
|
import { maxSatisfying, satisfies, validRange } from "semver";
|
|
19710
|
+
|
|
19711
|
+
// src/supply-chain/provenance.ts
|
|
19712
|
+
import { prerelease, rcompare, valid } from "semver";
|
|
18404
19713
|
var TIMEOUT_MS2 = 1500;
|
|
18405
|
-
var MAX_BYTES =
|
|
19714
|
+
var MAX_BYTES = 256 * 1024;
|
|
19715
|
+
function normalizeRepo(raw, host2 = "github.com") {
|
|
19716
|
+
let s = raw.trim().toLowerCase();
|
|
19717
|
+
s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
|
|
19718
|
+
s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
|
|
19719
|
+
if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
|
|
19720
|
+
return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
|
|
19721
|
+
}
|
|
19722
|
+
function npmRepoOf(doc) {
|
|
19723
|
+
const list = doc?.attestations;
|
|
19724
|
+
if (!Array.isArray(list)) return null;
|
|
19725
|
+
for (const a of list) {
|
|
19726
|
+
const att = a;
|
|
19727
|
+
if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
|
|
19728
|
+
const payload = att.bundle?.dsseEnvelope?.payload;
|
|
19729
|
+
if (typeof payload !== "string") continue;
|
|
19730
|
+
try {
|
|
19731
|
+
const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
|
19732
|
+
const p = statement.predicate;
|
|
19733
|
+
const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
|
|
19734
|
+
if (typeof repo === "string") return normalizeRepo(repo);
|
|
19735
|
+
} catch {
|
|
19736
|
+
}
|
|
19737
|
+
}
|
|
19738
|
+
return null;
|
|
19739
|
+
}
|
|
19740
|
+
function pypiRepoOf(doc) {
|
|
19741
|
+
const bundles = doc?.attestation_bundles;
|
|
19742
|
+
if (!Array.isArray(bundles)) return null;
|
|
19743
|
+
for (const b of bundles) {
|
|
19744
|
+
const pub = b.publisher;
|
|
19745
|
+
if (typeof pub?.repository === "string") {
|
|
19746
|
+
const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
|
|
19747
|
+
return normalizeRepo(pub.repository, host2);
|
|
19748
|
+
}
|
|
19749
|
+
if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
|
|
19750
|
+
return `google:${pub.email.trim().toLowerCase()}`;
|
|
19751
|
+
}
|
|
19752
|
+
}
|
|
19753
|
+
return null;
|
|
19754
|
+
}
|
|
19755
|
+
async function npmSourceRepo(name, version) {
|
|
19756
|
+
const base2 = endpoints().npmRegistry;
|
|
19757
|
+
if (!base2) return "unknown";
|
|
19758
|
+
const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
|
|
19759
|
+
return read(url, npmRepoOf);
|
|
19760
|
+
}
|
|
19761
|
+
async function read(url, repoOf) {
|
|
19762
|
+
try {
|
|
19763
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
|
|
19764
|
+
if (doc === null) return "none";
|
|
19765
|
+
return repoOf(doc) ?? "unknown";
|
|
19766
|
+
} catch {
|
|
19767
|
+
return "unknown";
|
|
19768
|
+
}
|
|
19769
|
+
}
|
|
19770
|
+
async function pypiSourceRepo(name, version, filename) {
|
|
19771
|
+
const base2 = endpoints().pypi;
|
|
19772
|
+
if (!base2) return "unknown";
|
|
19773
|
+
const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
|
|
19774
|
+
return read(url, pypiRepoOf);
|
|
19775
|
+
}
|
|
19776
|
+
var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
|
|
19777
|
+
function comparePypi(a, b) {
|
|
19778
|
+
const x = a.split(".").map(Number);
|
|
19779
|
+
const y = b.split(".").map(Number);
|
|
19780
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
19781
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
19782
|
+
if (d !== 0) return d;
|
|
19783
|
+
}
|
|
19784
|
+
return 0;
|
|
19785
|
+
}
|
|
19786
|
+
function previousRelease(eco, versions, version) {
|
|
19787
|
+
if (eco === "npm") {
|
|
19788
|
+
if (!valid(version)) return void 0;
|
|
19789
|
+
const below2 = versions.filter((v) => valid(v) && !prerelease(v) && rcompare(v, version) > 0);
|
|
19790
|
+
return below2.sort(rcompare)[0] ?? null;
|
|
19791
|
+
}
|
|
19792
|
+
if (!PYPI_FINAL.test(version)) return void 0;
|
|
19793
|
+
const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
|
|
19794
|
+
return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
|
|
19795
|
+
}
|
|
19796
|
+
|
|
19797
|
+
// src/supply-chain/registry.ts
|
|
19798
|
+
var TIMEOUT_MS3 = 1500;
|
|
19799
|
+
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
18406
19800
|
var VERSION_DOC_MAX_BYTES = 256 * 1024;
|
|
18407
19801
|
var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
18408
19802
|
async function npmVersionDocument(url, selector) {
|
|
18409
19803
|
const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
|
|
18410
|
-
timeoutMs:
|
|
19804
|
+
timeoutMs: TIMEOUT_MS3,
|
|
18411
19805
|
maxBytes: VERSION_DOC_MAX_BYTES
|
|
18412
19806
|
});
|
|
18413
19807
|
if (!doc?.version) return null;
|
|
@@ -18431,8 +19825,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18431
19825
|
let doc;
|
|
18432
19826
|
try {
|
|
18433
19827
|
doc = await fetchJson(url, {
|
|
18434
|
-
timeoutMs:
|
|
18435
|
-
maxBytes:
|
|
19828
|
+
timeoutMs: TIMEOUT_MS3,
|
|
19829
|
+
maxBytes: MAX_BYTES2,
|
|
18436
19830
|
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
18437
19831
|
});
|
|
18438
19832
|
} catch (err) {
|
|
@@ -18460,13 +19854,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
|
|
|
18460
19854
|
version: resolved,
|
|
18461
19855
|
hasInstallScript: v?.hasInstallScript === true
|
|
18462
19856
|
};
|
|
19857
|
+
const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
|
|
19858
|
+
if (v && previous !== void 0) {
|
|
19859
|
+
const pv = previous ? doc.versions?.[previous] : void 0;
|
|
19860
|
+
info.history = {
|
|
19861
|
+
previous,
|
|
19862
|
+
previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
|
|
19863
|
+
provenance: !!v.dist?.attestations?.provenance,
|
|
19864
|
+
previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
|
|
19865
|
+
};
|
|
19866
|
+
}
|
|
18463
19867
|
if (!Number.isFinite(modifiedMs)) return info;
|
|
18464
19868
|
if (Date.now() - modifiedMs >= freshWindowMs) {
|
|
18465
19869
|
info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
|
|
18466
19870
|
return info;
|
|
18467
19871
|
}
|
|
18468
19872
|
try {
|
|
18469
|
-
const full = await fetchJson(url, { timeoutMs:
|
|
19873
|
+
const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18470
19874
|
const t = full?.time?.[resolved];
|
|
18471
19875
|
const tMs = t ? Date.parse(t) : NaN;
|
|
18472
19876
|
if (Number.isFinite(tMs)) {
|
|
@@ -18484,15 +19888,37 @@ async function pypiInfo(name, version) {
|
|
|
18484
19888
|
const base2 = endpoints().pypi;
|
|
18485
19889
|
if (!base2) return null;
|
|
18486
19890
|
const url = version ? `${base2}/pypi/${encodeURIComponent(name)}/${encodeURIComponent(version)}/json` : `${base2}/pypi/${encodeURIComponent(name)}/json`;
|
|
18487
|
-
const doc = await fetchJson(url, { timeoutMs:
|
|
19891
|
+
const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
|
|
18488
19892
|
if (!doc?.info?.version) return null;
|
|
18489
19893
|
const times = (doc.urls ?? []).map((u) => u.upload_time_iso_8601 ? Date.parse(u.upload_time_iso_8601) : NaN).filter((t) => Number.isFinite(t));
|
|
18490
|
-
|
|
18491
|
-
|
|
19894
|
+
const resolved = version ?? doc.info.version;
|
|
19895
|
+
const info = {
|
|
19896
|
+
version: resolved,
|
|
18492
19897
|
// The earliest file of the release is when it became installable.
|
|
18493
|
-
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
|
|
19898
|
+
publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
|
|
19899
|
+
file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
|
|
19900
|
+
};
|
|
19901
|
+
if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
|
|
19902
|
+
return info;
|
|
19903
|
+
}
|
|
19904
|
+
function pypiHistoryOf(releases, version) {
|
|
19905
|
+
const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
|
|
19906
|
+
const previous = previousRelease("PyPI", versions, version);
|
|
19907
|
+
if (previous === void 0) return null;
|
|
19908
|
+
return {
|
|
19909
|
+
previous,
|
|
19910
|
+
previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
|
|
18494
19911
|
};
|
|
18495
19912
|
}
|
|
19913
|
+
async function pypiHistory(name, version) {
|
|
19914
|
+
const base2 = endpoints().pypi;
|
|
19915
|
+
if (!base2) return null;
|
|
19916
|
+
const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
|
|
19917
|
+
timeoutMs: TIMEOUT_MS3,
|
|
19918
|
+
maxBytes: MAX_BYTES2
|
|
19919
|
+
});
|
|
19920
|
+
return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
|
|
19921
|
+
}
|
|
18496
19922
|
var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
|
|
18497
19923
|
async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
18498
19924
|
if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
|
|
@@ -18501,19 +19927,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
18501
19927
|
}
|
|
18502
19928
|
|
|
18503
19929
|
// src/supply-chain/local-resolve.ts
|
|
18504
|
-
import
|
|
18505
|
-
import
|
|
19930
|
+
import fs26 from "fs";
|
|
19931
|
+
import path26 from "path";
|
|
18506
19932
|
var MAX_LEVELS = 32;
|
|
18507
19933
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
18508
19934
|
function resolveInstalled(name, cwd) {
|
|
18509
|
-
if (!cwd || !
|
|
19935
|
+
if (!cwd || !path26.isAbsolute(cwd)) return null;
|
|
18510
19936
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
18511
|
-
let dir2 =
|
|
19937
|
+
let dir2 = path26.resolve(cwd);
|
|
18512
19938
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
18513
|
-
const pkgDir =
|
|
18514
|
-
const manifest =
|
|
19939
|
+
const pkgDir = path26.join(dir2, "node_modules", ...name.split("/"));
|
|
19940
|
+
const manifest = path26.join(pkgDir, "package.json");
|
|
18515
19941
|
try {
|
|
18516
|
-
const raw =
|
|
19942
|
+
const raw = fs26.readFileSync(manifest, "utf8");
|
|
18517
19943
|
const parsed = JSON.parse(raw);
|
|
18518
19944
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
18519
19945
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -18521,7 +19947,7 @@ function resolveInstalled(name, cwd) {
|
|
|
18521
19947
|
return null;
|
|
18522
19948
|
} catch {
|
|
18523
19949
|
}
|
|
18524
|
-
const parent =
|
|
19950
|
+
const parent = path26.dirname(dir2);
|
|
18525
19951
|
if (parent === dir2) break;
|
|
18526
19952
|
dir2 = parent;
|
|
18527
19953
|
}
|
|
@@ -18643,7 +20069,7 @@ function checkInstalled(p, version, misses) {
|
|
|
18643
20069
|
}
|
|
18644
20070
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
18645
20071
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
18646
|
-
const localCwd = cwd &&
|
|
20072
|
+
const localCwd = cwd && path27.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path27.resolve(dir2, step), cwd) : void 0;
|
|
18647
20073
|
const installed = resolveInstalled(p.name, localCwd);
|
|
18648
20074
|
if (installed && (!p.version || p.version === installed.version))
|
|
18649
20075
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -18686,8 +20112,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
|
18686
20112
|
detail: `${label(p, version)} runs an install script`
|
|
18687
20113
|
});
|
|
18688
20114
|
}
|
|
20115
|
+
if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
|
|
20116
|
+
return byHistory(p, version, info, findings, misses);
|
|
20117
|
+
}
|
|
18689
20118
|
return findings;
|
|
18690
20119
|
}
|
|
20120
|
+
var SOFT = /* @__PURE__ */ new Set(["new", "install-script"]);
|
|
20121
|
+
async function byHistory(p, version, info, findings, misses) {
|
|
20122
|
+
let out = findings;
|
|
20123
|
+
let h = info.history;
|
|
20124
|
+
if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT.has(f.kind))) {
|
|
20125
|
+
try {
|
|
20126
|
+
h = await pypiHistory(p.name, version) ?? void 0;
|
|
20127
|
+
} catch (err) {
|
|
20128
|
+
misses.push(`${label(p, version)}: release history ${err.message}`);
|
|
20129
|
+
}
|
|
20130
|
+
}
|
|
20131
|
+
if (!h || !h.previous) return out;
|
|
20132
|
+
const previous = h.previous;
|
|
20133
|
+
if (h.previousHasInstallScript === true) {
|
|
20134
|
+
out = out.filter((f) => f.kind !== "install-script");
|
|
20135
|
+
}
|
|
20136
|
+
const drop = () => ({
|
|
20137
|
+
pkg: p,
|
|
20138
|
+
version,
|
|
20139
|
+
kind: "provenance-drop",
|
|
20140
|
+
detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
|
|
20141
|
+
});
|
|
20142
|
+
if (p.ecosystem === "npm") {
|
|
20143
|
+
if (h.previousProvenance && h.provenance === false) return [...out, drop()];
|
|
20144
|
+
if (!out.some((f) => SOFT.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
|
|
20145
|
+
const [now, before] = await Promise.all([
|
|
20146
|
+
npmSourceRepo(p.name, version),
|
|
20147
|
+
npmSourceRepo(p.name, previous)
|
|
20148
|
+
]);
|
|
20149
|
+
return vouched(p, version, now, before, out, misses);
|
|
20150
|
+
}
|
|
20151
|
+
if (p.ecosystem === "PyPI") {
|
|
20152
|
+
if (!out.some((f) => SOFT.has(f.kind)) || !info.file || !h.previousFile) return out;
|
|
20153
|
+
const [now, before] = await Promise.all([
|
|
20154
|
+
pypiSourceRepo(p.name, version, info.file),
|
|
20155
|
+
pypiSourceRepo(p.name, previous, h.previousFile)
|
|
20156
|
+
]);
|
|
20157
|
+
if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
|
|
20158
|
+
return vouched(p, version, now, before, out, misses);
|
|
20159
|
+
}
|
|
20160
|
+
return out;
|
|
20161
|
+
}
|
|
20162
|
+
function vouched(p, version, now, before, findings, misses) {
|
|
20163
|
+
if (now === "unknown" || before === "unknown") {
|
|
20164
|
+
misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
|
|
20165
|
+
return findings;
|
|
20166
|
+
}
|
|
20167
|
+
if (now === "none" || before === "none" || now !== before) return findings;
|
|
20168
|
+
return findings.filter((f) => !SOFT.has(f.kind));
|
|
20169
|
+
}
|
|
18691
20170
|
async function runPackageCheck(toolName, args, cfg, cwd) {
|
|
18692
20171
|
if (!cfg.enabled) return ALLOW;
|
|
18693
20172
|
try {
|
|
@@ -19070,7 +20549,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19070
20549
|
fileAccess,
|
|
19071
20550
|
config,
|
|
19072
20551
|
skipIgnoredFastPath,
|
|
19073
|
-
argumentScan: argScan
|
|
20552
|
+
argumentScan: argScan,
|
|
20553
|
+
// A server a person linked to an app is judged as that app (fw#390).
|
|
20554
|
+
mcpServer: resolveLinkedServer(meta?.mcpServer, {
|
|
20555
|
+
serverKey: meta?.serverKey,
|
|
20556
|
+
managedLinks: config.policy.appLinks,
|
|
20557
|
+
appliedShields: config.policy.appliedShields,
|
|
20558
|
+
workspaceGoverned: config.policySource === "workspace"
|
|
20559
|
+
})
|
|
19074
20560
|
});
|
|
19075
20561
|
if (!sessionScript || own.decision === "block") return own;
|
|
19076
20562
|
const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
|
|
@@ -19286,7 +20772,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19286
20772
|
const last = rec.sources[rec.sources.length - 1];
|
|
19287
20773
|
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
19288
20774
|
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
19289
|
-
const
|
|
20775
|
+
const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
19290
20776
|
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
19291
20777
|
const noun = (d) => {
|
|
19292
20778
|
const k = destinationKind(d);
|
|
@@ -19295,14 +20781,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
19295
20781
|
const parts = held.map(
|
|
19296
20782
|
(h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
|
|
19297
20783
|
);
|
|
19298
|
-
const home =
|
|
20784
|
+
const home = os20.homedir();
|
|
19299
20785
|
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
19300
20786
|
for (const w of writeHolds) {
|
|
19301
20787
|
parts.push(
|
|
19302
20788
|
w.why === "outside-project" ? `writes outside the project, to ${short(w.path)}` : w.why === "agent-instruction" ? `writes to ${short(w.path)}, a file that runs code or instructs the agent` : `writes content carrying ${destinationValue(w.destination ?? "")}, ${noun(w.destination ?? "host:x")} that appeared in that ${readWord}`
|
|
19303
20789
|
);
|
|
19304
20790
|
}
|
|
19305
|
-
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${
|
|
20791
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
|
|
19306
20792
|
exposureFromEid = last?.eid;
|
|
19307
20793
|
if (exposureFromEid) exposureMeta = { exposureFromEid };
|
|
19308
20794
|
exposureKeys = [
|