@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/dashboard.mjs
CHANGED
|
@@ -104,13 +104,99 @@ import picomatch from "picomatch";
|
|
|
104
104
|
import safeRegex from "safe-regex2";
|
|
105
105
|
import pm from "picomatch";
|
|
106
106
|
import mvdanSh from "mvdan-sh";
|
|
107
|
-
import safeRegex2 from "safe-regex2";
|
|
108
107
|
import mvdanSh2 from "mvdan-sh";
|
|
109
|
-
import
|
|
108
|
+
import safeRegex2 from "safe-regex2";
|
|
110
109
|
import { createHash as createHash2 } from "crypto";
|
|
111
110
|
import mvdanSh3 from "mvdan-sh";
|
|
112
111
|
import { createHash as createHash3 } from "crypto";
|
|
113
112
|
import mvdanSh4 from "mvdan-sh";
|
|
113
|
+
function validateRegex(pattern) {
|
|
114
|
+
if (!pattern) return "Pattern is required";
|
|
115
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
116
|
+
try {
|
|
117
|
+
new RegExp(pattern);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
120
|
+
}
|
|
121
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
122
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
126
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
127
|
+
const key = `${pattern}\0${flags}`;
|
|
128
|
+
if (regexCache.has(key)) {
|
|
129
|
+
const cached = regexCache.get(key);
|
|
130
|
+
regexCache.delete(key);
|
|
131
|
+
regexCache.set(key, cached);
|
|
132
|
+
return cached;
|
|
133
|
+
}
|
|
134
|
+
if (validateRegex(pattern) !== null) return null;
|
|
135
|
+
try {
|
|
136
|
+
const re = new RegExp(pattern, flags);
|
|
137
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
138
|
+
const oldest = regexCache.keys().next().value;
|
|
139
|
+
if (oldest) regexCache.delete(oldest);
|
|
140
|
+
}
|
|
141
|
+
regexCache.set(key, re);
|
|
142
|
+
return re;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function isToolRule(rule) {
|
|
148
|
+
return rule.tool.startsWith("{,mcp__*__}");
|
|
149
|
+
}
|
|
150
|
+
function actionPart(ruleName) {
|
|
151
|
+
const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
|
|
152
|
+
return m ? m[1] : ruleName ?? "rule";
|
|
153
|
+
}
|
|
154
|
+
function ownReason(reason, owner) {
|
|
155
|
+
if (!reason) return reason;
|
|
156
|
+
return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
|
|
157
|
+
}
|
|
158
|
+
function buildAppShield(spec) {
|
|
159
|
+
const owner = spec.label;
|
|
160
|
+
const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
|
|
161
|
+
...rule,
|
|
162
|
+
name: `shield:${spec.name}:${actionPart(rule.name)}`,
|
|
163
|
+
server: spec.server,
|
|
164
|
+
reason: ownReason(rule.reason, owner)
|
|
165
|
+
}));
|
|
166
|
+
const ownTools = (spec.mcpTools ?? []).map((rule) => ({
|
|
167
|
+
...rule,
|
|
168
|
+
server: spec.server
|
|
169
|
+
}));
|
|
170
|
+
return {
|
|
171
|
+
name: spec.name,
|
|
172
|
+
description: spec.description,
|
|
173
|
+
aliases: [],
|
|
174
|
+
smartRules: [...toolRules, ...ownTools, ...spec.doors],
|
|
175
|
+
dangerousWords: []
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function buildGeneralRules() {
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const name of Object.keys(ACTION_TEMPLATES)) {
|
|
181
|
+
const { slug: slug2, label } = ACTION_CHECKS[name];
|
|
182
|
+
for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
|
|
183
|
+
const part = actionPart(rule.name).replace(/^review-/, "");
|
|
184
|
+
out.push({
|
|
185
|
+
...rule,
|
|
186
|
+
name: `review-action-${slug2}-${part}`,
|
|
187
|
+
notServer: APP_SERVER_GLOB,
|
|
188
|
+
reason: ownReason(rule.reason, label)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function generalActionRuleNames(template) {
|
|
195
|
+
const { slug: slug2 } = ACTION_CHECKS[template];
|
|
196
|
+
return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
|
|
197
|
+
(r) => r.name
|
|
198
|
+
);
|
|
199
|
+
}
|
|
114
200
|
function isShieldVerdict(v) {
|
|
115
201
|
return v === "allow" || v === "review" || v === "block";
|
|
116
202
|
}
|
|
@@ -148,16 +234,25 @@ function validateOverrides(raw) {
|
|
|
148
234
|
return { overrides: result, warnings };
|
|
149
235
|
}
|
|
150
236
|
function assertBuiltinShieldRegexesAreSafe() {
|
|
151
|
-
|
|
237
|
+
const sets = [
|
|
238
|
+
...Object.values(BUILTIN_SHIELDS).map((sh) => ({
|
|
239
|
+
name: sh.name,
|
|
240
|
+
smartRules: sh.smartRules
|
|
241
|
+
})),
|
|
242
|
+
...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
|
|
243
|
+
{ name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
|
|
244
|
+
];
|
|
245
|
+
for (const shield of sets) {
|
|
152
246
|
for (const rule of shield.smartRules) {
|
|
153
247
|
const conditions = rule.conditions ?? [];
|
|
154
248
|
for (const cond of conditions) {
|
|
155
249
|
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
156
250
|
const pattern = cond.value;
|
|
157
251
|
if (!pattern) continue;
|
|
158
|
-
|
|
252
|
+
const problem = validateRegex(pattern);
|
|
253
|
+
if (problem) {
|
|
159
254
|
throw new Error(
|
|
160
|
-
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has
|
|
255
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
|
|
161
256
|
);
|
|
162
257
|
}
|
|
163
258
|
}
|
|
@@ -486,6 +581,55 @@ function cachedNormalize(command, compute) {
|
|
|
486
581
|
function normalizeCommandForPolicy(command) {
|
|
487
582
|
return commandReadingsImpl(command).posix;
|
|
488
583
|
}
|
|
584
|
+
function fileAccessView(command) {
|
|
585
|
+
const hit = accessViewCache.get(command);
|
|
586
|
+
if (hit !== void 0) return hit;
|
|
587
|
+
const view = fileAccessViewImpl(command);
|
|
588
|
+
if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
|
|
589
|
+
const oldest = accessViewCache.keys().next().value;
|
|
590
|
+
if (oldest !== void 0) accessViewCache.delete(oldest);
|
|
591
|
+
}
|
|
592
|
+
accessViewCache.set(command, view);
|
|
593
|
+
return view;
|
|
594
|
+
}
|
|
595
|
+
function fileAccessViewImpl(command) {
|
|
596
|
+
const text = command.replace(INTERPRETER_LINE, "");
|
|
597
|
+
const f = parseShared(text);
|
|
598
|
+
if (f === PARSE_FAIL) return text;
|
|
599
|
+
const toCharIndex = byteOffsetToCharIndex(text);
|
|
600
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
601
|
+
const edits = [];
|
|
602
|
+
try {
|
|
603
|
+
syntax.Walk(f, (node) => {
|
|
604
|
+
if (!node) return false;
|
|
605
|
+
const n = node;
|
|
606
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
607
|
+
const args = n.Args || [];
|
|
608
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
609
|
+
const head = unwrapCommandHead(words);
|
|
610
|
+
for (const i of /* @__PURE__ */ new Set([0, head])) {
|
|
611
|
+
const word = words[i];
|
|
612
|
+
if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (!PROGRAM_DIR.test(word)) continue;
|
|
616
|
+
const name = word.split(/[\\/]/).pop() ?? "";
|
|
617
|
+
if (!name) continue;
|
|
618
|
+
const s = at(args[i].Pos().Offset());
|
|
619
|
+
const e = at(args[i].End().Offset());
|
|
620
|
+
if (s < 0 || e < 0) continue;
|
|
621
|
+
edits.push([s, e, name]);
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
});
|
|
625
|
+
} catch {
|
|
626
|
+
return text;
|
|
627
|
+
}
|
|
628
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
629
|
+
let out = text;
|
|
630
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
489
633
|
function commandReadings(command) {
|
|
490
634
|
const r = commandReadingsImpl(command);
|
|
491
635
|
const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
|
|
@@ -1728,40 +1872,6 @@ function jailedRedirectRead(stmt) {
|
|
|
1728
1872
|
}
|
|
1729
1873
|
return null;
|
|
1730
1874
|
}
|
|
1731
|
-
function validateRegex(pattern) {
|
|
1732
|
-
if (!pattern) return "Pattern is required";
|
|
1733
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1734
|
-
try {
|
|
1735
|
-
new RegExp(pattern);
|
|
1736
|
-
} catch (e) {
|
|
1737
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
1738
|
-
}
|
|
1739
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1740
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1741
|
-
return null;
|
|
1742
|
-
}
|
|
1743
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
1744
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1745
|
-
const key = `${pattern}\0${flags}`;
|
|
1746
|
-
if (regexCache.has(key)) {
|
|
1747
|
-
const cached = regexCache.get(key);
|
|
1748
|
-
regexCache.delete(key);
|
|
1749
|
-
regexCache.set(key, cached);
|
|
1750
|
-
return cached;
|
|
1751
|
-
}
|
|
1752
|
-
if (validateRegex(pattern) !== null) return null;
|
|
1753
|
-
try {
|
|
1754
|
-
const re = new RegExp(pattern, flags);
|
|
1755
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1756
|
-
const oldest = regexCache.keys().next().value;
|
|
1757
|
-
if (oldest) regexCache.delete(oldest);
|
|
1758
|
-
}
|
|
1759
|
-
regexCache.set(key, re);
|
|
1760
|
-
return re;
|
|
1761
|
-
} catch {
|
|
1762
|
-
return null;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
1875
|
function matchesPattern(text, patterns) {
|
|
1766
1876
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
1767
1877
|
if (p.length === 0) return false;
|
|
@@ -1794,23 +1904,26 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1794
1904
|
return val;
|
|
1795
1905
|
};
|
|
1796
1906
|
const readingsCache = /* @__PURE__ */ new Map();
|
|
1797
|
-
const resolveFieldReadings = (field) => {
|
|
1798
|
-
const
|
|
1907
|
+
const resolveFieldReadings = (field, reading) => {
|
|
1908
|
+
const access = reading === "access" && field === "command";
|
|
1909
|
+
const key = access ? `${field}\0access` : field;
|
|
1910
|
+
const cached = readingsCache.get(key);
|
|
1799
1911
|
if (cached) return cached;
|
|
1800
1912
|
const primary = resolveField(field);
|
|
1801
1913
|
if (primary === null) {
|
|
1802
|
-
readingsCache.set(
|
|
1914
|
+
readingsCache.set(key, []);
|
|
1803
1915
|
return [];
|
|
1804
1916
|
}
|
|
1805
1917
|
let out = [primary];
|
|
1806
1918
|
if (field === "command") {
|
|
1807
1919
|
const raw = getNestedValue(args, field);
|
|
1808
1920
|
if (typeof raw === "string") {
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1921
|
+
const text = access ? fileAccessView(raw) : raw;
|
|
1922
|
+
const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
|
|
1923
|
+
out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
1811
1924
|
}
|
|
1812
1925
|
}
|
|
1813
|
-
readingsCache.set(
|
|
1926
|
+
readingsCache.set(key, out);
|
|
1814
1927
|
return out;
|
|
1815
1928
|
};
|
|
1816
1929
|
const results = rule.conditions.map((cond) => {
|
|
@@ -1828,14 +1941,14 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1828
1941
|
if (val === null || !cond.value) return false;
|
|
1829
1942
|
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1830
1943
|
if (!reM) return false;
|
|
1831
|
-
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
1944
|
+
return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
|
|
1832
1945
|
}
|
|
1833
1946
|
case "notMatches": {
|
|
1834
1947
|
if (!cond.value) return false;
|
|
1835
1948
|
if (val === null) return true;
|
|
1836
1949
|
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1837
1950
|
if (!reN) return false;
|
|
1838
|
-
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
1951
|
+
return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
|
|
1839
1952
|
}
|
|
1840
1953
|
case "matchesGlob":
|
|
1841
1954
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
@@ -2218,14 +2331,14 @@ function validateOtpSecret(raw) {
|
|
|
2218
2331
|
}
|
|
2219
2332
|
function assertBuiltinPatternsAreSafe() {
|
|
2220
2333
|
for (const p of DLP_PATTERNS) {
|
|
2221
|
-
if (!
|
|
2334
|
+
if (!safeRegex2(p.regex.source)) {
|
|
2222
2335
|
throw new Error(
|
|
2223
2336
|
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
2224
2337
|
);
|
|
2225
2338
|
}
|
|
2226
2339
|
}
|
|
2227
2340
|
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
2228
|
-
if (!
|
|
2341
|
+
if (!safeRegex2(re.source)) {
|
|
2229
2342
|
throw new Error(
|
|
2230
2343
|
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
2231
2344
|
);
|
|
@@ -2691,10 +2804,13 @@ function matchCanaryArgs(args, values) {
|
|
|
2691
2804
|
return null;
|
|
2692
2805
|
}
|
|
2693
2806
|
}
|
|
2694
|
-
var aws_default, bash_safe_default, docker_default, filesystem_default,
|
|
2807
|
+
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, METADATA_HOSTNAMES, v4Octets, 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, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, SYSTEM_DIRS, UPLOAD_SYSTEM_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, 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, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, 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, 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, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, BIP39_ENGLISH, indices, 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, 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, syntax3, EDITOR_HEADS, MANAGER_HEADS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, SOURCE_COMMANDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, MASKED_KINDS, ENTRY_RE, syntax4, PUBLISH, OWN_HEADS, TERMINAL_ESCAPE_RE, 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;
|
|
2695
2808
|
var init_dist = __esm({
|
|
2696
2809
|
"packages/policy-engine/dist/index.mjs"() {
|
|
2697
2810
|
"use strict";
|
|
2811
|
+
MAX_REGEX_LENGTH = 256;
|
|
2812
|
+
REGEX_CACHE_MAX = 500;
|
|
2813
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
2698
2814
|
aws_default = {
|
|
2699
2815
|
name: "aws",
|
|
2700
2816
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -2996,77 +3112,6 @@ var init_dist = __esm({
|
|
|
2996
3112
|
],
|
|
2997
3113
|
dangerousWords: ["wipefs"]
|
|
2998
3114
|
};
|
|
2999
|
-
finance_default = {
|
|
3000
|
-
name: "finance",
|
|
3001
|
-
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.",
|
|
3002
|
-
aliases: ["payments", "money"],
|
|
3003
|
-
smartRules: [
|
|
3004
|
-
{
|
|
3005
|
-
name: "shield:finance:review-refund",
|
|
3006
|
-
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
3007
|
-
conditions: [],
|
|
3008
|
-
verdict: "review",
|
|
3009
|
-
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
3010
|
-
},
|
|
3011
|
-
{
|
|
3012
|
-
name: "shield:finance:review-payout",
|
|
3013
|
-
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
3014
|
-
conditions: [],
|
|
3015
|
-
verdict: "review",
|
|
3016
|
-
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
3017
|
-
},
|
|
3018
|
-
{
|
|
3019
|
-
name: "shield:finance:review-transfer",
|
|
3020
|
-
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}",
|
|
3021
|
-
conditions: [],
|
|
3022
|
-
verdict: "review",
|
|
3023
|
-
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
3024
|
-
},
|
|
3025
|
-
{
|
|
3026
|
-
name: "shield:finance:review-charge",
|
|
3027
|
-
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}",
|
|
3028
|
-
conditions: [],
|
|
3029
|
-
verdict: "review",
|
|
3030
|
-
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
3031
|
-
},
|
|
3032
|
-
{
|
|
3033
|
-
name: "shield:finance:review-payment-link",
|
|
3034
|
-
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
3035
|
-
conditions: [],
|
|
3036
|
-
verdict: "review",
|
|
3037
|
-
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
3038
|
-
},
|
|
3039
|
-
{
|
|
3040
|
-
name: "shield:finance:review-subscription",
|
|
3041
|
-
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
3042
|
-
conditions: [],
|
|
3043
|
-
verdict: "review",
|
|
3044
|
-
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
3045
|
-
},
|
|
3046
|
-
{
|
|
3047
|
-
name: "shield:finance:review-dispute",
|
|
3048
|
-
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
3049
|
-
conditions: [],
|
|
3050
|
-
verdict: "review",
|
|
3051
|
-
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
3052
|
-
},
|
|
3053
|
-
{
|
|
3054
|
-
name: "shield:finance:review-card",
|
|
3055
|
-
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
3056
|
-
conditions: [],
|
|
3057
|
-
verdict: "review",
|
|
3058
|
-
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
3059
|
-
},
|
|
3060
|
-
{
|
|
3061
|
-
name: "shield:finance:review-withdraw",
|
|
3062
|
-
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
3063
|
-
conditions: [],
|
|
3064
|
-
verdict: "review",
|
|
3065
|
-
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
3066
|
-
}
|
|
3067
|
-
],
|
|
3068
|
-
dangerousWords: []
|
|
3069
|
-
};
|
|
3070
3115
|
github_default = {
|
|
3071
3116
|
name: "github",
|
|
3072
3117
|
description: "Protects GitHub repositories from destructive AI operations",
|
|
@@ -3093,56 +3138,6 @@ var init_dist = __esm({
|
|
|
3093
3138
|
],
|
|
3094
3139
|
dangerousWords: []
|
|
3095
3140
|
};
|
|
3096
|
-
identity_default = {
|
|
3097
|
-
name: "identity",
|
|
3098
|
-
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.",
|
|
3099
|
-
aliases: ["access", "iam"],
|
|
3100
|
-
smartRules: [
|
|
3101
|
-
{
|
|
3102
|
-
name: "shield:identity:review-password",
|
|
3103
|
-
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
3104
|
-
conditions: [],
|
|
3105
|
-
verdict: "review",
|
|
3106
|
-
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
3107
|
-
},
|
|
3108
|
-
{
|
|
3109
|
-
name: "shield:identity:review-key",
|
|
3110
|
-
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}",
|
|
3111
|
-
conditions: [],
|
|
3112
|
-
verdict: "review",
|
|
3113
|
-
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
3114
|
-
},
|
|
3115
|
-
{
|
|
3116
|
-
name: "shield:identity:review-access",
|
|
3117
|
-
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}",
|
|
3118
|
-
conditions: [],
|
|
3119
|
-
verdict: "review",
|
|
3120
|
-
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
3121
|
-
},
|
|
3122
|
-
{
|
|
3123
|
-
name: "shield:identity:review-membership",
|
|
3124
|
-
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}",
|
|
3125
|
-
conditions: [],
|
|
3126
|
-
verdict: "review",
|
|
3127
|
-
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
3128
|
-
},
|
|
3129
|
-
{
|
|
3130
|
-
name: "shield:identity:review-user",
|
|
3131
|
-
tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
|
|
3132
|
-
conditions: [],
|
|
3133
|
-
verdict: "review",
|
|
3134
|
-
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
3135
|
-
},
|
|
3136
|
-
{
|
|
3137
|
-
name: "shield:identity:review-mfa",
|
|
3138
|
-
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
3139
|
-
conditions: [],
|
|
3140
|
-
verdict: "review",
|
|
3141
|
-
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
3142
|
-
}
|
|
3143
|
-
],
|
|
3144
|
-
dangerousWords: []
|
|
3145
|
-
};
|
|
3146
3141
|
k8s_default = {
|
|
3147
3142
|
name: "k8s",
|
|
3148
3143
|
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
@@ -3395,7 +3390,7 @@ var init_dist = __esm({
|
|
|
3395
3390
|
{
|
|
3396
3391
|
field: "command",
|
|
3397
3392
|
op: "matches",
|
|
3398
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
3393
|
+
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;&|>)<]|$)",
|
|
3399
3394
|
flags: "i"
|
|
3400
3395
|
}
|
|
3401
3396
|
],
|
|
@@ -3409,7 +3404,7 @@ var init_dist = __esm({
|
|
|
3409
3404
|
{
|
|
3410
3405
|
field: "command",
|
|
3411
3406
|
op: "matches",
|
|
3412
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim
|
|
3407
|
+
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)",
|
|
3413
3408
|
flags: "i"
|
|
3414
3409
|
}
|
|
3415
3410
|
],
|
|
@@ -3475,20 +3470,220 @@ var init_dist = __esm({
|
|
|
3475
3470
|
],
|
|
3476
3471
|
dangerousWords: []
|
|
3477
3472
|
};
|
|
3478
|
-
|
|
3479
|
-
name: "
|
|
3480
|
-
description: "
|
|
3481
|
-
aliases: [
|
|
3473
|
+
redis_default = {
|
|
3474
|
+
name: "redis",
|
|
3475
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
3476
|
+
aliases: [],
|
|
3477
|
+
_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.",
|
|
3482
3478
|
smartRules: [
|
|
3483
3479
|
{
|
|
3484
|
-
name: "shield:
|
|
3485
|
-
tool: "
|
|
3486
|
-
conditions: [
|
|
3487
|
-
|
|
3488
|
-
|
|
3480
|
+
name: "shield:redis:block-flushall",
|
|
3481
|
+
tool: "*",
|
|
3482
|
+
conditions: [
|
|
3483
|
+
{
|
|
3484
|
+
field: "command",
|
|
3485
|
+
op: "matches",
|
|
3486
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
3487
|
+
flags: "i"
|
|
3488
|
+
}
|
|
3489
|
+
],
|
|
3490
|
+
verdict: "block",
|
|
3491
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
3489
3492
|
},
|
|
3490
3493
|
{
|
|
3491
|
-
name: "shield:
|
|
3494
|
+
name: "shield:redis:block-flushdb",
|
|
3495
|
+
tool: "*",
|
|
3496
|
+
conditions: [
|
|
3497
|
+
{
|
|
3498
|
+
field: "command",
|
|
3499
|
+
op: "matches",
|
|
3500
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
3501
|
+
flags: "i"
|
|
3502
|
+
}
|
|
3503
|
+
],
|
|
3504
|
+
verdict: "block",
|
|
3505
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
3506
|
+
},
|
|
3507
|
+
{
|
|
3508
|
+
name: "shield:redis:block-config-resetstat",
|
|
3509
|
+
tool: "*",
|
|
3510
|
+
conditions: [
|
|
3511
|
+
{
|
|
3512
|
+
field: "command",
|
|
3513
|
+
op: "matches",
|
|
3514
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
3515
|
+
flags: "i"
|
|
3516
|
+
}
|
|
3517
|
+
],
|
|
3518
|
+
verdict: "block",
|
|
3519
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
3520
|
+
},
|
|
3521
|
+
{
|
|
3522
|
+
name: "shield:redis:review-config-set",
|
|
3523
|
+
tool: "*",
|
|
3524
|
+
conditions: [
|
|
3525
|
+
{
|
|
3526
|
+
field: "command",
|
|
3527
|
+
op: "matches",
|
|
3528
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
3529
|
+
flags: "i"
|
|
3530
|
+
}
|
|
3531
|
+
],
|
|
3532
|
+
verdict: "review",
|
|
3533
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
3534
|
+
},
|
|
3535
|
+
{
|
|
3536
|
+
name: "shield:redis:review-del-wildcard",
|
|
3537
|
+
tool: "*",
|
|
3538
|
+
conditions: [
|
|
3539
|
+
{
|
|
3540
|
+
field: "command",
|
|
3541
|
+
op: "matches",
|
|
3542
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
3543
|
+
flags: "i"
|
|
3544
|
+
}
|
|
3545
|
+
],
|
|
3546
|
+
verdict: "review",
|
|
3547
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
3548
|
+
}
|
|
3549
|
+
],
|
|
3550
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
3551
|
+
};
|
|
3552
|
+
finance_default = {
|
|
3553
|
+
name: "finance",
|
|
3554
|
+
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.",
|
|
3555
|
+
aliases: ["payments", "money"],
|
|
3556
|
+
smartRules: [
|
|
3557
|
+
{
|
|
3558
|
+
name: "shield:finance:review-refund",
|
|
3559
|
+
tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
|
|
3560
|
+
conditions: [],
|
|
3561
|
+
verdict: "review",
|
|
3562
|
+
reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
|
|
3563
|
+
},
|
|
3564
|
+
{
|
|
3565
|
+
name: "shield:finance:review-payout",
|
|
3566
|
+
tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
|
|
3567
|
+
conditions: [],
|
|
3568
|
+
verdict: "review",
|
|
3569
|
+
reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
name: "shield:finance:review-transfer",
|
|
3573
|
+
tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
|
|
3574
|
+
conditions: [],
|
|
3575
|
+
verdict: "review",
|
|
3576
|
+
reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
|
|
3577
|
+
},
|
|
3578
|
+
{
|
|
3579
|
+
name: "shield:finance:review-charge",
|
|
3580
|
+
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}",
|
|
3581
|
+
conditions: [],
|
|
3582
|
+
verdict: "review",
|
|
3583
|
+
reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
|
|
3584
|
+
},
|
|
3585
|
+
{
|
|
3586
|
+
name: "shield:finance:review-payment-link",
|
|
3587
|
+
tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
|
|
3588
|
+
conditions: [],
|
|
3589
|
+
verdict: "review",
|
|
3590
|
+
reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
|
|
3591
|
+
},
|
|
3592
|
+
{
|
|
3593
|
+
name: "shield:finance:review-subscription",
|
|
3594
|
+
tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
|
|
3595
|
+
conditions: [],
|
|
3596
|
+
verdict: "review",
|
|
3597
|
+
reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
name: "shield:finance:review-dispute",
|
|
3601
|
+
tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
|
|
3602
|
+
conditions: [],
|
|
3603
|
+
verdict: "review",
|
|
3604
|
+
reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
|
|
3605
|
+
},
|
|
3606
|
+
{
|
|
3607
|
+
name: "shield:finance:review-card",
|
|
3608
|
+
tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
|
|
3609
|
+
conditions: [],
|
|
3610
|
+
verdict: "review",
|
|
3611
|
+
reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
name: "shield:finance:review-withdraw",
|
|
3615
|
+
tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
|
|
3616
|
+
conditions: [],
|
|
3617
|
+
verdict: "review",
|
|
3618
|
+
reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
|
|
3619
|
+
}
|
|
3620
|
+
],
|
|
3621
|
+
dangerousWords: []
|
|
3622
|
+
};
|
|
3623
|
+
identity_default = {
|
|
3624
|
+
name: "identity",
|
|
3625
|
+
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.",
|
|
3626
|
+
aliases: ["access", "iam"],
|
|
3627
|
+
smartRules: [
|
|
3628
|
+
{
|
|
3629
|
+
name: "shield:identity:review-password",
|
|
3630
|
+
tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
|
|
3631
|
+
conditions: [],
|
|
3632
|
+
verdict: "review",
|
|
3633
|
+
reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
|
|
3634
|
+
},
|
|
3635
|
+
{
|
|
3636
|
+
name: "shield:identity:review-key",
|
|
3637
|
+
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}",
|
|
3638
|
+
conditions: [],
|
|
3639
|
+
verdict: "review",
|
|
3640
|
+
reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
|
|
3641
|
+
},
|
|
3642
|
+
{
|
|
3643
|
+
name: "shield:identity:review-access",
|
|
3644
|
+
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}",
|
|
3645
|
+
conditions: [],
|
|
3646
|
+
verdict: "review",
|
|
3647
|
+
reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
|
|
3648
|
+
},
|
|
3649
|
+
{
|
|
3650
|
+
name: "shield:identity:review-membership",
|
|
3651
|
+
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}",
|
|
3652
|
+
conditions: [],
|
|
3653
|
+
verdict: "review",
|
|
3654
|
+
reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
|
|
3655
|
+
},
|
|
3656
|
+
{
|
|
3657
|
+
name: "shield:identity:review-user",
|
|
3658
|
+
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}",
|
|
3659
|
+
conditions: [],
|
|
3660
|
+
verdict: "review",
|
|
3661
|
+
reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
|
|
3662
|
+
},
|
|
3663
|
+
{
|
|
3664
|
+
name: "shield:identity:review-mfa",
|
|
3665
|
+
tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
|
|
3666
|
+
conditions: [],
|
|
3667
|
+
verdict: "review",
|
|
3668
|
+
reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
|
|
3669
|
+
}
|
|
3670
|
+
],
|
|
3671
|
+
dangerousWords: []
|
|
3672
|
+
};
|
|
3673
|
+
publishing_default = {
|
|
3674
|
+
name: "publishing",
|
|
3675
|
+
description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
|
|
3676
|
+
aliases: ["release", "deploy"],
|
|
3677
|
+
smartRules: [
|
|
3678
|
+
{
|
|
3679
|
+
name: "shield:publishing:review-release",
|
|
3680
|
+
tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
|
|
3681
|
+
conditions: [],
|
|
3682
|
+
verdict: "review",
|
|
3683
|
+
reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
name: "shield:publishing:review-package",
|
|
3492
3687
|
tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
|
|
3493
3688
|
conditions: [],
|
|
3494
3689
|
verdict: "review",
|
|
@@ -3644,109 +3839,511 @@ var init_dist = __esm({
|
|
|
3644
3839
|
],
|
|
3645
3840
|
dangerousWords: []
|
|
3646
3841
|
};
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
{
|
|
3672
|
-
field: "command",
|
|
3673
|
-
op: "matches",
|
|
3674
|
-
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
3675
|
-
flags: "i"
|
|
3676
|
-
}
|
|
3677
|
-
],
|
|
3678
|
-
verdict: "block",
|
|
3679
|
-
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
3680
|
-
},
|
|
3681
|
-
{
|
|
3682
|
-
name: "shield:redis:block-config-resetstat",
|
|
3683
|
-
tool: "*",
|
|
3684
|
-
conditions: [
|
|
3685
|
-
{
|
|
3686
|
-
field: "command",
|
|
3687
|
-
op: "matches",
|
|
3688
|
-
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
3689
|
-
flags: "i"
|
|
3690
|
-
}
|
|
3691
|
-
],
|
|
3692
|
-
verdict: "block",
|
|
3693
|
-
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
3694
|
-
},
|
|
3695
|
-
{
|
|
3696
|
-
name: "shield:redis:review-config-set",
|
|
3697
|
-
tool: "*",
|
|
3698
|
-
conditions: [
|
|
3699
|
-
{
|
|
3700
|
-
field: "command",
|
|
3701
|
-
op: "matches",
|
|
3702
|
-
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
3703
|
-
flags: "i"
|
|
3704
|
-
}
|
|
3705
|
-
],
|
|
3706
|
-
verdict: "review",
|
|
3707
|
-
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
3708
|
-
},
|
|
3709
|
-
{
|
|
3710
|
-
name: "shield:redis:review-del-wildcard",
|
|
3711
|
-
tool: "*",
|
|
3712
|
-
conditions: [
|
|
3713
|
-
{
|
|
3714
|
-
field: "command",
|
|
3715
|
-
op: "matches",
|
|
3716
|
-
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
3717
|
-
flags: "i"
|
|
3718
|
-
}
|
|
3719
|
-
],
|
|
3720
|
-
verdict: "review",
|
|
3721
|
-
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
3722
|
-
}
|
|
3723
|
-
],
|
|
3724
|
-
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
3842
|
+
ACTION_TEMPLATES = {
|
|
3843
|
+
finance: finance_default.smartRules,
|
|
3844
|
+
identity: identity_default.smartRules,
|
|
3845
|
+
publishing: publishing_default.smartRules
|
|
3846
|
+
};
|
|
3847
|
+
ACTION_CHECKS = {
|
|
3848
|
+
finance: { checkId: "actions.money", slug: "money", label: "Money check" },
|
|
3849
|
+
identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
|
|
3850
|
+
publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
|
|
3851
|
+
};
|
|
3852
|
+
GAP = "[^;&|]{0,200}?";
|
|
3853
|
+
prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
|
|
3854
|
+
httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
|
|
3855
|
+
httpWrite = (methods) => ({
|
|
3856
|
+
field: "command",
|
|
3857
|
+
op: "matches",
|
|
3858
|
+
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)`
|
|
3859
|
+
});
|
|
3860
|
+
HTTP_WRITE = httpWrite("POST|DELETE");
|
|
3861
|
+
HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
|
|
3862
|
+
NOT_STRIPE_TEST = {
|
|
3863
|
+
field: "command",
|
|
3864
|
+
op: "notMatches",
|
|
3865
|
+
value: "\\b[sr]k_test_"
|
|
3725
3866
|
};
|
|
3867
|
+
postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
|
|
3868
|
+
STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
|
|
3869
|
+
APP_SHIELD_SPECS = [
|
|
3870
|
+
{
|
|
3871
|
+
name: "stripe",
|
|
3872
|
+
label: "Stripe",
|
|
3873
|
+
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.",
|
|
3874
|
+
template: "finance",
|
|
3875
|
+
server: "*stripe*",
|
|
3876
|
+
doors: [
|
|
3877
|
+
{
|
|
3878
|
+
name: "shield:stripe:review-cli",
|
|
3879
|
+
tool: "bash",
|
|
3880
|
+
conditions: [
|
|
3881
|
+
{
|
|
3882
|
+
field: "command",
|
|
3883
|
+
op: "matches",
|
|
3884
|
+
value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
|
|
3885
|
+
flags: "i"
|
|
3886
|
+
},
|
|
3887
|
+
{
|
|
3888
|
+
field: "command",
|
|
3889
|
+
op: "matches",
|
|
3890
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
3891
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
3892
|
+
}
|
|
3893
|
+
],
|
|
3894
|
+
conditionMode: "all",
|
|
3895
|
+
verdict: "review",
|
|
3896
|
+
reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
|
|
3897
|
+
},
|
|
3898
|
+
{
|
|
3899
|
+
name: "shield:stripe:review-cli-request",
|
|
3900
|
+
tool: "bash",
|
|
3901
|
+
conditions: [
|
|
3902
|
+
{
|
|
3903
|
+
field: "command",
|
|
3904
|
+
op: "matches",
|
|
3905
|
+
value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
|
|
3906
|
+
flags: "i"
|
|
3907
|
+
},
|
|
3908
|
+
{
|
|
3909
|
+
field: "command",
|
|
3910
|
+
op: "matches",
|
|
3911
|
+
// The stripe CLI works in test mode unless told otherwise.
|
|
3912
|
+
value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
|
|
3913
|
+
}
|
|
3914
|
+
],
|
|
3915
|
+
conditionMode: "all",
|
|
3916
|
+
verdict: "review",
|
|
3917
|
+
reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
|
|
3918
|
+
},
|
|
3919
|
+
{
|
|
3920
|
+
name: "shield:stripe:review-api",
|
|
3921
|
+
tool: "bash",
|
|
3922
|
+
conditions: [
|
|
3923
|
+
{
|
|
3924
|
+
field: "command",
|
|
3925
|
+
op: "matches",
|
|
3926
|
+
value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
|
|
3927
|
+
flags: "i"
|
|
3928
|
+
},
|
|
3929
|
+
HTTP_WRITE,
|
|
3930
|
+
NOT_STRIPE_TEST
|
|
3931
|
+
],
|
|
3932
|
+
conditionMode: "all",
|
|
3933
|
+
verdict: "review",
|
|
3934
|
+
reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
|
|
3935
|
+
},
|
|
3936
|
+
{
|
|
3937
|
+
name: "shield:stripe:review-code",
|
|
3938
|
+
tool: "bash",
|
|
3939
|
+
conditions: [
|
|
3940
|
+
{
|
|
3941
|
+
field: "command",
|
|
3942
|
+
op: "matches",
|
|
3943
|
+
// Python: stripe.Refund.create(...)
|
|
3944
|
+
value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
|
|
3945
|
+
},
|
|
3946
|
+
NOT_STRIPE_TEST
|
|
3947
|
+
],
|
|
3948
|
+
conditionMode: "all",
|
|
3949
|
+
verdict: "review",
|
|
3950
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
3951
|
+
},
|
|
3952
|
+
{
|
|
3953
|
+
name: "shield:stripe:review-code-node",
|
|
3954
|
+
tool: "bash",
|
|
3955
|
+
conditions: [
|
|
3956
|
+
{
|
|
3957
|
+
field: "command",
|
|
3958
|
+
op: "matches",
|
|
3959
|
+
// Node: stripe.refunds.create(...)
|
|
3960
|
+
value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
|
|
3961
|
+
},
|
|
3962
|
+
NOT_STRIPE_TEST
|
|
3963
|
+
],
|
|
3964
|
+
conditionMode: "all",
|
|
3965
|
+
verdict: "review",
|
|
3966
|
+
reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
|
|
3967
|
+
},
|
|
3968
|
+
{
|
|
3969
|
+
name: "shield:stripe:review-code-http",
|
|
3970
|
+
tool: "bash",
|
|
3971
|
+
conditions: [
|
|
3972
|
+
{
|
|
3973
|
+
field: "command",
|
|
3974
|
+
op: "matches",
|
|
3975
|
+
// requests.post("https://api.stripe.com/v1/refunds", ...)
|
|
3976
|
+
value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
|
|
3977
|
+
},
|
|
3978
|
+
NOT_STRIPE_TEST
|
|
3979
|
+
],
|
|
3980
|
+
conditionMode: "all",
|
|
3981
|
+
verdict: "review",
|
|
3982
|
+
reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
|
|
3983
|
+
}
|
|
3984
|
+
]
|
|
3985
|
+
},
|
|
3986
|
+
{
|
|
3987
|
+
name: "paypal",
|
|
3988
|
+
label: "PayPal",
|
|
3989
|
+
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.",
|
|
3990
|
+
template: "finance",
|
|
3991
|
+
server: "*paypal*",
|
|
3992
|
+
doors: [
|
|
3993
|
+
{
|
|
3994
|
+
name: "shield:paypal:review-api",
|
|
3995
|
+
tool: "bash",
|
|
3996
|
+
conditions: [
|
|
3997
|
+
{
|
|
3998
|
+
field: "command",
|
|
3999
|
+
op: "matches",
|
|
4000
|
+
// The live hosts only: api.paypal.com and api-m.paypal.com. The
|
|
4001
|
+
// sandbox (api-m.sandbox.paypal.com) moves no money.
|
|
4002
|
+
value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
|
|
4003
|
+
flags: "i"
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
field: "command",
|
|
4007
|
+
op: "matches",
|
|
4008
|
+
// The endpoints that move money.
|
|
4009
|
+
value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
|
|
4010
|
+
flags: "i"
|
|
4011
|
+
},
|
|
4012
|
+
HTTP_WRITE
|
|
4013
|
+
],
|
|
4014
|
+
conditionMode: "all",
|
|
4015
|
+
verdict: "review",
|
|
4016
|
+
reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
name: "shield:paypal:review-code",
|
|
4020
|
+
tool: "bash",
|
|
4021
|
+
conditions: [
|
|
4022
|
+
{
|
|
4023
|
+
field: "command",
|
|
4024
|
+
op: "matches",
|
|
4025
|
+
// The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
|
|
4026
|
+
// Python paypalrestsdk.
|
|
4027
|
+
value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
|
|
4028
|
+
}
|
|
4029
|
+
],
|
|
4030
|
+
verdict: "review",
|
|
4031
|
+
reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
|
|
4032
|
+
},
|
|
4033
|
+
{
|
|
4034
|
+
name: "shield:paypal:review-code-http",
|
|
4035
|
+
tool: "bash",
|
|
4036
|
+
conditions: [
|
|
4037
|
+
{
|
|
4038
|
+
field: "command",
|
|
4039
|
+
op: "matches",
|
|
4040
|
+
// The endpoints that move money; the token call every script
|
|
4041
|
+
// starts with (/v1/oauth2/token) is not one.
|
|
4042
|
+
value: postTo(
|
|
4043
|
+
"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)"
|
|
4044
|
+
)
|
|
4045
|
+
}
|
|
4046
|
+
],
|
|
4047
|
+
verdict: "review",
|
|
4048
|
+
reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
|
|
4049
|
+
}
|
|
4050
|
+
]
|
|
4051
|
+
},
|
|
4052
|
+
{
|
|
4053
|
+
name: "square",
|
|
4054
|
+
label: "Square",
|
|
4055
|
+
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.",
|
|
4056
|
+
template: "finance",
|
|
4057
|
+
// Square's servers, never Squarespace (a website builder).
|
|
4058
|
+
server: "{*square,*square[-_]*,*squareup*}",
|
|
4059
|
+
mcpTools: [
|
|
4060
|
+
{
|
|
4061
|
+
// Square's official MCP server has one tool for every API call; the
|
|
4062
|
+
// service and method name what it does.
|
|
4063
|
+
name: "shield:square:review-mcp-request",
|
|
4064
|
+
tool: "{,mcp__*__}make_api_request",
|
|
4065
|
+
conditions: [
|
|
4066
|
+
{
|
|
4067
|
+
field: "service",
|
|
4068
|
+
op: "matches",
|
|
4069
|
+
value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
|
|
4070
|
+
flags: "i"
|
|
4071
|
+
},
|
|
4072
|
+
{
|
|
4073
|
+
field: "method",
|
|
4074
|
+
op: "notMatches",
|
|
4075
|
+
value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
|
|
4076
|
+
flags: "i"
|
|
4077
|
+
}
|
|
4078
|
+
],
|
|
4079
|
+
conditionMode: "all",
|
|
4080
|
+
verdict: "review",
|
|
4081
|
+
reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
|
|
4082
|
+
}
|
|
4083
|
+
],
|
|
4084
|
+
doors: [
|
|
4085
|
+
{
|
|
4086
|
+
name: "shield:square:review-api",
|
|
4087
|
+
tool: "bash",
|
|
4088
|
+
conditions: [
|
|
4089
|
+
{
|
|
4090
|
+
field: "command",
|
|
4091
|
+
op: "matches",
|
|
4092
|
+
// The live host only; the sandbox is connect.squareupsandbox.com.
|
|
4093
|
+
value: httpTo(
|
|
4094
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
4095
|
+
),
|
|
4096
|
+
flags: "i"
|
|
4097
|
+
},
|
|
4098
|
+
HTTP_WRITE_ANY
|
|
4099
|
+
],
|
|
4100
|
+
conditionMode: "all",
|
|
4101
|
+
verdict: "review",
|
|
4102
|
+
reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
|
|
4103
|
+
},
|
|
4104
|
+
{
|
|
4105
|
+
name: "shield:square:review-code",
|
|
4106
|
+
tool: "bash",
|
|
4107
|
+
conditions: [
|
|
4108
|
+
{
|
|
4109
|
+
field: "command",
|
|
4110
|
+
op: "matches",
|
|
4111
|
+
// The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
|
|
4112
|
+
value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
|
|
4113
|
+
},
|
|
4114
|
+
{
|
|
4115
|
+
field: "command",
|
|
4116
|
+
op: "matches",
|
|
4117
|
+
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*\\("
|
|
4118
|
+
}
|
|
4119
|
+
],
|
|
4120
|
+
conditionMode: "all",
|
|
4121
|
+
verdict: "review",
|
|
4122
|
+
reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
|
|
4123
|
+
},
|
|
4124
|
+
{
|
|
4125
|
+
name: "shield:square:review-code-http",
|
|
4126
|
+
tool: "bash",
|
|
4127
|
+
conditions: [
|
|
4128
|
+
{
|
|
4129
|
+
field: "command",
|
|
4130
|
+
op: "matches",
|
|
4131
|
+
value: postTo(
|
|
4132
|
+
"connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
|
|
4133
|
+
)
|
|
4134
|
+
}
|
|
4135
|
+
],
|
|
4136
|
+
verdict: "review",
|
|
4137
|
+
reason: "This code posts to the Square API. node9 asks before money moves (Square)."
|
|
4138
|
+
}
|
|
4139
|
+
]
|
|
4140
|
+
},
|
|
4141
|
+
{
|
|
4142
|
+
name: "plaid",
|
|
4143
|
+
label: "Plaid",
|
|
4144
|
+
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.",
|
|
4145
|
+
template: "finance",
|
|
4146
|
+
server: "*plaid*",
|
|
4147
|
+
doors: [
|
|
4148
|
+
{
|
|
4149
|
+
name: "shield:plaid:review-api",
|
|
4150
|
+
tool: "bash",
|
|
4151
|
+
conditions: [
|
|
4152
|
+
{
|
|
4153
|
+
field: "command",
|
|
4154
|
+
op: "matches",
|
|
4155
|
+
// Every Plaid endpoint is a POST, so the path is what tells a
|
|
4156
|
+
// write; the live host only, never sandbox.plaid.com. A script
|
|
4157
|
+
// that posts to the URL meets this door too.
|
|
4158
|
+
value: httpTo(
|
|
4159
|
+
"production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
|
|
4160
|
+
),
|
|
4161
|
+
flags: "i"
|
|
4162
|
+
}
|
|
4163
|
+
],
|
|
4164
|
+
verdict: "review",
|
|
4165
|
+
reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
|
|
4166
|
+
},
|
|
4167
|
+
{
|
|
4168
|
+
name: "shield:plaid:review-code",
|
|
4169
|
+
tool: "bash",
|
|
4170
|
+
conditions: [
|
|
4171
|
+
{ field: "command", op: "matches", value: "\\bplaid", flags: "i" },
|
|
4172
|
+
{
|
|
4173
|
+
field: "command",
|
|
4174
|
+
op: "matches",
|
|
4175
|
+
value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
|
|
4176
|
+
}
|
|
4177
|
+
],
|
|
4178
|
+
conditionMode: "all",
|
|
4179
|
+
verdict: "review",
|
|
4180
|
+
reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
|
|
4181
|
+
}
|
|
4182
|
+
]
|
|
4183
|
+
},
|
|
4184
|
+
{
|
|
4185
|
+
name: "okta",
|
|
4186
|
+
label: "Okta",
|
|
4187
|
+
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.",
|
|
4188
|
+
template: "identity",
|
|
4189
|
+
server: "*okta*",
|
|
4190
|
+
doors: [
|
|
4191
|
+
{
|
|
4192
|
+
name: "shield:okta:review-api",
|
|
4193
|
+
tool: "bash",
|
|
4194
|
+
conditions: [
|
|
4195
|
+
{
|
|
4196
|
+
field: "command",
|
|
4197
|
+
op: "matches",
|
|
4198
|
+
// The management API only: sign-in (/api/v1/authn, /sessions,
|
|
4199
|
+
// /oauth2) is not under these paths.
|
|
4200
|
+
value: httpTo(
|
|
4201
|
+
"[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
|
|
4202
|
+
),
|
|
4203
|
+
flags: "i"
|
|
4204
|
+
},
|
|
4205
|
+
HTTP_WRITE_ANY
|
|
4206
|
+
],
|
|
4207
|
+
conditionMode: "all",
|
|
4208
|
+
verdict: "review",
|
|
4209
|
+
reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
|
|
4210
|
+
},
|
|
4211
|
+
{
|
|
4212
|
+
name: "shield:okta:review-code",
|
|
4213
|
+
tool: "bash",
|
|
4214
|
+
conditions: [
|
|
4215
|
+
{
|
|
4216
|
+
field: "command",
|
|
4217
|
+
op: "matches",
|
|
4218
|
+
// The Okta SDK in the script: a method named createUser alone is
|
|
4219
|
+
// in half the applications ever written.
|
|
4220
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
4221
|
+
},
|
|
4222
|
+
{
|
|
4223
|
+
field: "command",
|
|
4224
|
+
op: "matches",
|
|
4225
|
+
value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
|
|
4226
|
+
}
|
|
4227
|
+
],
|
|
4228
|
+
conditionMode: "all",
|
|
4229
|
+
verdict: "review",
|
|
4230
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
4231
|
+
},
|
|
4232
|
+
{
|
|
4233
|
+
name: "shield:okta:review-code-py",
|
|
4234
|
+
tool: "bash",
|
|
4235
|
+
conditions: [
|
|
4236
|
+
{
|
|
4237
|
+
field: "command",
|
|
4238
|
+
op: "matches",
|
|
4239
|
+
value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
|
|
4240
|
+
},
|
|
4241
|
+
{
|
|
4242
|
+
field: "command",
|
|
4243
|
+
op: "matches",
|
|
4244
|
+
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*\\("
|
|
4245
|
+
}
|
|
4246
|
+
],
|
|
4247
|
+
conditionMode: "all",
|
|
4248
|
+
verdict: "review",
|
|
4249
|
+
reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
|
|
4250
|
+
}
|
|
4251
|
+
]
|
|
4252
|
+
},
|
|
4253
|
+
{
|
|
4254
|
+
name: "auth0",
|
|
4255
|
+
label: "Auth0",
|
|
4256
|
+
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.",
|
|
4257
|
+
template: "identity",
|
|
4258
|
+
server: "*auth0*",
|
|
4259
|
+
mcpTools: [
|
|
4260
|
+
{
|
|
4261
|
+
// Auth0's official MCP server: applications (their secrets and
|
|
4262
|
+
// callbacks), APIs and their scopes, and Actions (code that runs at
|
|
4263
|
+
// every sign-in).
|
|
4264
|
+
name: "shield:auth0:review-tenant-config",
|
|
4265
|
+
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}",
|
|
4266
|
+
conditions: [],
|
|
4267
|
+
verdict: "review",
|
|
4268
|
+
reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
|
|
4269
|
+
}
|
|
4270
|
+
],
|
|
4271
|
+
doors: [
|
|
4272
|
+
{
|
|
4273
|
+
name: "shield:auth0:review-cli",
|
|
4274
|
+
tool: "bash",
|
|
4275
|
+
conditions: [
|
|
4276
|
+
{
|
|
4277
|
+
field: "command",
|
|
4278
|
+
op: "matches",
|
|
4279
|
+
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-])",
|
|
4280
|
+
flags: "i"
|
|
4281
|
+
}
|
|
4282
|
+
],
|
|
4283
|
+
verdict: "review",
|
|
4284
|
+
reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
|
|
4285
|
+
},
|
|
4286
|
+
{
|
|
4287
|
+
name: "shield:auth0:review-api",
|
|
4288
|
+
tool: "bash",
|
|
4289
|
+
conditions: [
|
|
4290
|
+
{
|
|
4291
|
+
field: "command",
|
|
4292
|
+
op: "matches",
|
|
4293
|
+
// The Management API only; /oauth/token is signing in.
|
|
4294
|
+
value: httpTo(
|
|
4295
|
+
"\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
|
|
4296
|
+
),
|
|
4297
|
+
flags: "i"
|
|
4298
|
+
},
|
|
4299
|
+
HTTP_WRITE_ANY
|
|
4300
|
+
],
|
|
4301
|
+
conditionMode: "all",
|
|
4302
|
+
verdict: "review",
|
|
4303
|
+
reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
|
|
4304
|
+
},
|
|
4305
|
+
{
|
|
4306
|
+
name: "shield:auth0:review-code",
|
|
4307
|
+
tool: "bash",
|
|
4308
|
+
conditions: [
|
|
4309
|
+
{
|
|
4310
|
+
field: "command",
|
|
4311
|
+
op: "matches",
|
|
4312
|
+
// The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
|
|
4313
|
+
value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
|
|
4314
|
+
},
|
|
4315
|
+
{
|
|
4316
|
+
field: "command",
|
|
4317
|
+
op: "matches",
|
|
4318
|
+
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*\\("
|
|
4319
|
+
}
|
|
4320
|
+
],
|
|
4321
|
+
conditionMode: "all",
|
|
4322
|
+
verdict: "review",
|
|
4323
|
+
reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
|
|
4324
|
+
}
|
|
4325
|
+
]
|
|
4326
|
+
}
|
|
4327
|
+
];
|
|
4328
|
+
APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
|
|
4329
|
+
APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
|
|
4330
|
+
APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
|
|
4331
|
+
GENERAL_ACTION_RULES = buildGeneralRules();
|
|
3726
4332
|
BUILTIN_SHIELDS = {
|
|
3727
4333
|
[aws_default.name]: aws_default,
|
|
3728
4334
|
[bash_safe_default.name]: bash_safe_default,
|
|
3729
4335
|
[docker_default.name]: docker_default,
|
|
3730
4336
|
[filesystem_default.name]: filesystem_default,
|
|
3731
|
-
// Dangerous actions as category shields (node9Firewall#385): an MCP app's
|
|
3732
|
-
// money, credential and publishing tools, asked about whatever the request
|
|
3733
|
-
// said. One glob per rule covers the bare name the gateway sends and the
|
|
3734
|
-
// mcp__<server>__ name the agent hook sends.
|
|
3735
|
-
[finance_default.name]: finance_default,
|
|
3736
4337
|
[github_default.name]: github_default,
|
|
3737
|
-
[identity_default.name]: identity_default,
|
|
3738
4338
|
[k8s_default.name]: k8s_default,
|
|
3739
4339
|
[mongodb_default.name]: mongodb_default,
|
|
3740
4340
|
[postgres_default.name]: postgres_default,
|
|
3741
4341
|
[project_jail_default.name]: project_jail_default,
|
|
3742
|
-
[
|
|
3743
|
-
|
|
4342
|
+
[redis_default.name]: redis_default,
|
|
4343
|
+
// App shields generated from the action templates (node9Firewall#390): one
|
|
4344
|
+
// per app, every door (MCP server, CLI, API, library), off by default.
|
|
4345
|
+
...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
|
|
3744
4346
|
};
|
|
3745
|
-
ACTION_SHIELD_NAMES = /* @__PURE__ */ new Set([
|
|
3746
|
-
"finance",
|
|
3747
|
-
"identity",
|
|
3748
|
-
"publishing"
|
|
3749
|
-
]);
|
|
3750
4347
|
assertBuiltinShieldRegexesAreSafe();
|
|
3751
4348
|
SSRF_MAX_HOST = 253;
|
|
3752
4349
|
METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
@@ -3792,9 +4389,9 @@ var init_dist = __esm({
|
|
|
3792
4389
|
},
|
|
3793
4390
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
3794
4391
|
"commands.inline-exec": {
|
|
3795
|
-
plain: "The agent runs a small program written inside the command itself.",
|
|
4392
|
+
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.",
|
|
3796
4393
|
example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
|
|
3797
|
-
advice: "
|
|
4394
|
+
advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
|
|
3798
4395
|
},
|
|
3799
4396
|
"commands.eval-dynamic": {
|
|
3800
4397
|
plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
|
|
@@ -3826,6 +4423,21 @@ var init_dist = __esm({
|
|
|
3826
4423
|
example: "chmod 777 deploy.sh",
|
|
3827
4424
|
advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
|
|
3828
4425
|
},
|
|
4426
|
+
"actions.money": {
|
|
4427
|
+
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.",
|
|
4428
|
+
example: "refund_order on a server called acme-billing",
|
|
4429
|
+
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."
|
|
4430
|
+
},
|
|
4431
|
+
"actions.access": {
|
|
4432
|
+
plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
|
|
4433
|
+
example: "invite_user or grant_role on a company admin server",
|
|
4434
|
+
advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
|
|
4435
|
+
},
|
|
4436
|
+
"actions.publish": {
|
|
4437
|
+
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.",
|
|
4438
|
+
example: "create_release or deploy_to_production on a company MCP server",
|
|
4439
|
+
advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
|
|
4440
|
+
},
|
|
3829
4441
|
"commands.sudo": {
|
|
3830
4442
|
plain: "The agent runs a command as the administrator (root).",
|
|
3831
4443
|
example: "sudo apt-get install -y nginx",
|
|
@@ -3986,7 +4598,7 @@ var init_dist = __esm({
|
|
|
3986
4598
|
advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
|
|
3987
4599
|
},
|
|
3988
4600
|
"loading.malicious-package": {
|
|
3989
|
-
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.",
|
|
4601
|
+
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.",
|
|
3990
4602
|
example: "npm install of a package flagged as malware",
|
|
3991
4603
|
advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
|
|
3992
4604
|
}
|
|
@@ -4038,7 +4650,7 @@ var init_dist = __esm({
|
|
|
4038
4650
|
id: "commands.inline-exec",
|
|
4039
4651
|
group: "commands",
|
|
4040
4652
|
title: "Inline code execution",
|
|
4041
|
-
catches: "
|
|
4653
|
+
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",
|
|
4042
4654
|
values: ALL,
|
|
4043
4655
|
defaultValue: "review",
|
|
4044
4656
|
legacy: ["Node9 Standard (Inline Execution)"]
|
|
@@ -4395,6 +5007,38 @@ var init_dist = __esm({
|
|
|
4395
5007
|
values: ALL,
|
|
4396
5008
|
defaultValue: "block",
|
|
4397
5009
|
legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
|
|
5010
|
+
},
|
|
5011
|
+
// ── Actions in apps (node9Firewall#390) ──────────────────────────────────
|
|
5012
|
+
// The safety net under the app shields: an action that moves money, changes
|
|
5013
|
+
// access or publishes, on an MCP server no app shield claims (an internal
|
|
5014
|
+
// billing server, a vendor with no shield, Zapier). Review by default; the
|
|
5015
|
+
// rules come from the action templates (shields/apps.ts).
|
|
5016
|
+
{
|
|
5017
|
+
id: "actions.money",
|
|
5018
|
+
group: "actions",
|
|
5019
|
+
title: "Money moves",
|
|
5020
|
+
catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
|
|
5021
|
+
values: ALL,
|
|
5022
|
+
defaultValue: "review",
|
|
5023
|
+
legacy: generalActionRuleNames("finance")
|
|
5024
|
+
},
|
|
5025
|
+
{
|
|
5026
|
+
id: "actions.access",
|
|
5027
|
+
group: "actions",
|
|
5028
|
+
title: "Accounts and access change",
|
|
5029
|
+
catches: "a password, key, role, permission or user changed through an app with no protection of its own",
|
|
5030
|
+
values: ALL,
|
|
5031
|
+
defaultValue: "review",
|
|
5032
|
+
legacy: generalActionRuleNames("identity")
|
|
5033
|
+
},
|
|
5034
|
+
{
|
|
5035
|
+
id: "actions.publish",
|
|
5036
|
+
group: "actions",
|
|
5037
|
+
title: "Something is published",
|
|
5038
|
+
catches: "a release, package, deploy or public post through an app with no protection of its own",
|
|
5039
|
+
values: ALL,
|
|
5040
|
+
defaultValue: "review",
|
|
5041
|
+
legacy: generalActionRuleNames("publishing")
|
|
4398
5042
|
}
|
|
4399
5043
|
];
|
|
4400
5044
|
SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
|
|
@@ -4515,6 +5159,170 @@ var init_dist = __esm({
|
|
|
4515
5159
|
\u03A7: "X"
|
|
4516
5160
|
};
|
|
4517
5161
|
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
5162
|
+
SYSTEM_DIRS = [
|
|
5163
|
+
"/usr",
|
|
5164
|
+
"/etc",
|
|
5165
|
+
"/boot",
|
|
5166
|
+
"/bin",
|
|
5167
|
+
"/sbin",
|
|
5168
|
+
"/lib",
|
|
5169
|
+
"/lib64",
|
|
5170
|
+
"/var/log"
|
|
5171
|
+
];
|
|
5172
|
+
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
5173
|
+
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
5174
|
+
MAX_INLINE_CODE = 64 * 1024;
|
|
5175
|
+
OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
|
|
5176
|
+
pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
|
|
5177
|
+
either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
|
|
5178
|
+
PYTHON = [
|
|
5179
|
+
[
|
|
5180
|
+
"deletes",
|
|
5181
|
+
/\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/
|
|
5182
|
+
],
|
|
5183
|
+
[
|
|
5184
|
+
"writes-outside",
|
|
5185
|
+
new RegExp(
|
|
5186
|
+
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}`
|
|
5187
|
+
)
|
|
5188
|
+
],
|
|
5189
|
+
[
|
|
5190
|
+
"sends",
|
|
5191
|
+
either(
|
|
5192
|
+
pyImport(
|
|
5193
|
+
String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
|
|
5194
|
+
),
|
|
5195
|
+
/\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
|
|
5196
|
+
)
|
|
5197
|
+
],
|
|
5198
|
+
[
|
|
5199
|
+
"listens",
|
|
5200
|
+
either(
|
|
5201
|
+
pyImport(String.raw`pty|socketserver|http\.server`),
|
|
5202
|
+
/\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
|
|
5203
|
+
)
|
|
5204
|
+
],
|
|
5205
|
+
[
|
|
5206
|
+
"runs-commands",
|
|
5207
|
+
either(
|
|
5208
|
+
pyImport("subprocess|pexpect|commands|psutil"),
|
|
5209
|
+
/\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+\*/
|
|
5210
|
+
)
|
|
5211
|
+
],
|
|
5212
|
+
[
|
|
5213
|
+
"builds-code",
|
|
5214
|
+
/(?<![\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*\[/
|
|
5215
|
+
],
|
|
5216
|
+
[
|
|
5217
|
+
// Spyware shapes: a screenshot, the clipboard, a key logger.
|
|
5218
|
+
"monitors",
|
|
5219
|
+
either(
|
|
5220
|
+
pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
|
|
5221
|
+
/\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
|
|
5222
|
+
)
|
|
5223
|
+
],
|
|
5224
|
+
[
|
|
5225
|
+
"permissions",
|
|
5226
|
+
/\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
|
|
5227
|
+
]
|
|
5228
|
+
];
|
|
5229
|
+
NODE = [
|
|
5230
|
+
[
|
|
5231
|
+
"deletes",
|
|
5232
|
+
/\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
|
|
5233
|
+
],
|
|
5234
|
+
[
|
|
5235
|
+
"writes-outside",
|
|
5236
|
+
new RegExp(
|
|
5237
|
+
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}`
|
|
5238
|
+
)
|
|
5239
|
+
],
|
|
5240
|
+
[
|
|
5241
|
+
"sends",
|
|
5242
|
+
/\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*\(/
|
|
5243
|
+
],
|
|
5244
|
+
["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
|
|
5245
|
+
[
|
|
5246
|
+
"runs-commands",
|
|
5247
|
+
/\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
|
|
5248
|
+
],
|
|
5249
|
+
[
|
|
5250
|
+
"builds-code",
|
|
5251
|
+
/(?<![\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)]/
|
|
5252
|
+
],
|
|
5253
|
+
[
|
|
5254
|
+
"permissions",
|
|
5255
|
+
/\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
|
|
5256
|
+
]
|
|
5257
|
+
];
|
|
5258
|
+
PERL = [
|
|
5259
|
+
["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
|
|
5260
|
+
[
|
|
5261
|
+
"writes-outside",
|
|
5262
|
+
new RegExp(
|
|
5263
|
+
String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
|
|
5264
|
+
)
|
|
5265
|
+
],
|
|
5266
|
+
[
|
|
5267
|
+
"sends",
|
|
5268
|
+
/\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
|
|
5269
|
+
],
|
|
5270
|
+
[
|
|
5271
|
+
"runs-commands",
|
|
5272
|
+
/\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
|
|
5273
|
+
],
|
|
5274
|
+
["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
|
|
5275
|
+
["permissions", /\b(?:chmod|chown)\b/]
|
|
5276
|
+
];
|
|
5277
|
+
RUBY = [
|
|
5278
|
+
[
|
|
5279
|
+
"deletes",
|
|
5280
|
+
/\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
|
|
5281
|
+
],
|
|
5282
|
+
[
|
|
5283
|
+
"writes-outside",
|
|
5284
|
+
new RegExp(
|
|
5285
|
+
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}`
|
|
5286
|
+
)
|
|
5287
|
+
],
|
|
5288
|
+
[
|
|
5289
|
+
"sends",
|
|
5290
|
+
/\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)['"]/
|
|
5291
|
+
],
|
|
5292
|
+
["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
|
|
5293
|
+
[
|
|
5294
|
+
"runs-commands",
|
|
5295
|
+
/(?<![\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/
|
|
5296
|
+
],
|
|
5297
|
+
[
|
|
5298
|
+
"builds-code",
|
|
5299
|
+
/(?<![\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/
|
|
5300
|
+
],
|
|
5301
|
+
["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
|
|
5302
|
+
];
|
|
5303
|
+
PHP = [
|
|
5304
|
+
["deletes", /\b(?:unlink|rmdir)\s*\(/],
|
|
5305
|
+
[
|
|
5306
|
+
"writes-outside",
|
|
5307
|
+
new RegExp(
|
|
5308
|
+
String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
|
|
5309
|
+
)
|
|
5310
|
+
],
|
|
5311
|
+
[
|
|
5312
|
+
"sends",
|
|
5313
|
+
/\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):/
|
|
5314
|
+
],
|
|
5315
|
+
["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
|
|
5316
|
+
[
|
|
5317
|
+
"runs-commands",
|
|
5318
|
+
/\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
|
|
5319
|
+
],
|
|
5320
|
+
[
|
|
5321
|
+
"builds-code",
|
|
5322
|
+
/\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
|
|
5323
|
+
],
|
|
5324
|
+
["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
|
|
5325
|
+
];
|
|
4518
5326
|
({ syntax } = mvdanSh);
|
|
4519
5327
|
sharedParser = syntax.NewParser();
|
|
4520
5328
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -4552,6 +5360,10 @@ var init_dist = __esm({
|
|
|
4552
5360
|
AST_CACHE_MAX = 5e3;
|
|
4553
5361
|
astCache = /* @__PURE__ */ new Map();
|
|
4554
5362
|
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
5363
|
+
INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
|
|
5364
|
+
PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
|
|
5365
|
+
ACCESS_VIEW_CACHE_MAX = 1e3;
|
|
5366
|
+
accessViewCache = /* @__PURE__ */ new Map();
|
|
4555
5367
|
MAX_READING_PAYLOADS = 8;
|
|
4556
5368
|
defaultExpansionOps = null;
|
|
4557
5369
|
VARIABLE_READERS = /* @__PURE__ */ new Set([
|
|
@@ -5285,9 +6097,6 @@ var init_dist = __esm({
|
|
|
5285
6097
|
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
5286
6098
|
NONE = { kind: "none" };
|
|
5287
6099
|
UNKNOWN = { kind: "unknown" };
|
|
5288
|
-
MAX_REGEX_LENGTH = 256;
|
|
5289
|
-
REGEX_CACHE_MAX = 500;
|
|
5290
|
-
regexCache = /* @__PURE__ */ new Map();
|
|
5291
6100
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5292
6101
|
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5293
6102
|
"bash",
|
|
@@ -5325,18 +6134,6 @@ var init_dist = __esm({
|
|
|
5325
6134
|
(s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
|
|
5326
6135
|
)
|
|
5327
6136
|
);
|
|
5328
|
-
SYSTEM_DIRS = [
|
|
5329
|
-
"/usr",
|
|
5330
|
-
"/etc",
|
|
5331
|
-
"/boot",
|
|
5332
|
-
"/bin",
|
|
5333
|
-
"/sbin",
|
|
5334
|
-
"/lib",
|
|
5335
|
-
"/lib64",
|
|
5336
|
-
"/var/log"
|
|
5337
|
-
];
|
|
5338
|
-
UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
|
|
5339
|
-
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
5340
6137
|
BIP39_ENGLISH = `abandon
|
|
5341
6138
|
ability
|
|
5342
6139
|
able
|
|
@@ -8377,7 +9174,11 @@ zoo`.split("\n");
|
|
|
8377
9174
|
"commands.disk-destroy",
|
|
8378
9175
|
"commands.dangerous-word",
|
|
8379
9176
|
"commands.node9-disarm",
|
|
8380
|
-
"data.pipe-chain"
|
|
9177
|
+
"data.pipe-chain",
|
|
9178
|
+
// The general action checks (node9Firewall#390): smart rules the map governs.
|
|
9179
|
+
"actions.money",
|
|
9180
|
+
"actions.access",
|
|
9181
|
+
"actions.publish"
|
|
8381
9182
|
]);
|
|
8382
9183
|
KNOB_GOVERNED = {
|
|
8383
9184
|
"data.secrets": ["off", "block"],
|
|
@@ -9230,7 +10031,8 @@ var init_config_schema = __esm({
|
|
|
9230
10031
|
}
|
|
9231
10032
|
),
|
|
9232
10033
|
value: z.string().optional(),
|
|
9233
|
-
flags: z.string().optional()
|
|
10034
|
+
flags: z.string().optional(),
|
|
10035
|
+
reading: z.enum(["access"]).optional()
|
|
9234
10036
|
}).refine(
|
|
9235
10037
|
(c) => {
|
|
9236
10038
|
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
@@ -9241,6 +10043,9 @@ var init_config_schema = __esm({
|
|
|
9241
10043
|
SmartRuleSchema = z.object({
|
|
9242
10044
|
name: z.string().optional(),
|
|
9243
10045
|
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
10046
|
+
// Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
|
|
10047
|
+
server: z.string().min(1).optional(),
|
|
10048
|
+
notServer: z.string().min(1).optional(),
|
|
9244
10049
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
9245
10050
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
9246
10051
|
verdict: z.enum(["allow", "review", "block"], {
|
|
@@ -9391,6 +10196,82 @@ var init_config_schema = __esm({
|
|
|
9391
10196
|
}
|
|
9392
10197
|
});
|
|
9393
10198
|
|
|
10199
|
+
// src/shields/build.ts
|
|
10200
|
+
function escapeRegex(s) {
|
|
10201
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10202
|
+
}
|
|
10203
|
+
function slug(s) {
|
|
10204
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
10205
|
+
}
|
|
10206
|
+
function pathToRegexFragment(rawPath) {
|
|
10207
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
10208
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
10209
|
+
if (segments2.length === 0) return "";
|
|
10210
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
10211
|
+
}
|
|
10212
|
+
function pathRules(rawPath, verdict, reason) {
|
|
10213
|
+
const value = pathToRegexFragment(rawPath);
|
|
10214
|
+
if (!value) return [];
|
|
10215
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
10216
|
+
const s = slug(rawPath);
|
|
10217
|
+
return [
|
|
10218
|
+
// `reading: 'access'`: a script's interpreter line and the program a
|
|
10219
|
+
// command runs are not reads of the jailed path (node9Firewall#385).
|
|
10220
|
+
{
|
|
10221
|
+
name: `${verdict}-path-${s}-bash`,
|
|
10222
|
+
tool: "bash",
|
|
10223
|
+
conditions: [{ field: "command", op: "matches", value, reading: "access" }],
|
|
10224
|
+
verdict,
|
|
10225
|
+
reason: why
|
|
10226
|
+
},
|
|
10227
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
10228
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
10229
|
+
{
|
|
10230
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
10231
|
+
tool: "*",
|
|
10232
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
10233
|
+
verdict,
|
|
10234
|
+
reason: why
|
|
10235
|
+
},
|
|
10236
|
+
{
|
|
10237
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
10238
|
+
tool: "*",
|
|
10239
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
10240
|
+
verdict,
|
|
10241
|
+
reason: why
|
|
10242
|
+
},
|
|
10243
|
+
{
|
|
10244
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
10245
|
+
tool: "*",
|
|
10246
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
10247
|
+
verdict,
|
|
10248
|
+
reason: why
|
|
10249
|
+
}
|
|
10250
|
+
];
|
|
10251
|
+
}
|
|
10252
|
+
function withAccessReading(def) {
|
|
10253
|
+
let changed = false;
|
|
10254
|
+
const smartRules = def.smartRules.map((r) => {
|
|
10255
|
+
const [c, ...rest] = r.conditions ?? [];
|
|
10256
|
+
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) {
|
|
10257
|
+
return r;
|
|
10258
|
+
}
|
|
10259
|
+
changed = true;
|
|
10260
|
+
return { ...r, conditions: [{ ...c, reading: "access" }] };
|
|
10261
|
+
});
|
|
10262
|
+
return changed ? { ...def, smartRules } : def;
|
|
10263
|
+
}
|
|
10264
|
+
var B, SEP, LEGACY_BASH_PATH_RULE;
|
|
10265
|
+
var init_build = __esm({
|
|
10266
|
+
"src/shields/build.ts"() {
|
|
10267
|
+
"use strict";
|
|
10268
|
+
init_dist();
|
|
10269
|
+
B = "[\\s/\\\\]";
|
|
10270
|
+
SEP = "[/\\\\]";
|
|
10271
|
+
LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
|
|
10272
|
+
}
|
|
10273
|
+
});
|
|
10274
|
+
|
|
9394
10275
|
// src/shields.ts
|
|
9395
10276
|
import fs5 from "fs";
|
|
9396
10277
|
import path6 from "path";
|
|
@@ -9419,7 +10300,7 @@ function loadUserShields() {
|
|
|
9419
10300
|
`);
|
|
9420
10301
|
continue;
|
|
9421
10302
|
}
|
|
9422
|
-
result[v.ok.name] = v.ok;
|
|
10303
|
+
result[v.ok.name] = withAccessReading(v.ok);
|
|
9423
10304
|
} catch (err) {
|
|
9424
10305
|
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
9425
10306
|
`);
|
|
@@ -9475,6 +10356,7 @@ var init_shields = __esm({
|
|
|
9475
10356
|
"src/shields.ts"() {
|
|
9476
10357
|
"use strict";
|
|
9477
10358
|
init_dist();
|
|
10359
|
+
init_build();
|
|
9478
10360
|
init_dist();
|
|
9479
10361
|
USER_SHIELDS_DIR = path6.join(os6.homedir(), ".node9", "shields");
|
|
9480
10362
|
SHIELDS = buildSHIELDS();
|
|
@@ -9610,67 +10492,6 @@ var init_managed = __esm({
|
|
|
9610
10492
|
}
|
|
9611
10493
|
});
|
|
9612
10494
|
|
|
9613
|
-
// src/shields/build.ts
|
|
9614
|
-
function escapeRegex(s) {
|
|
9615
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9616
|
-
}
|
|
9617
|
-
function slug(s) {
|
|
9618
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
9619
|
-
}
|
|
9620
|
-
function pathToRegexFragment(rawPath) {
|
|
9621
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
9622
|
-
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
9623
|
-
if (segments2.length === 0) return "";
|
|
9624
|
-
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
9625
|
-
}
|
|
9626
|
-
function pathRules(rawPath, verdict, reason) {
|
|
9627
|
-
const value = pathToRegexFragment(rawPath);
|
|
9628
|
-
if (!value) return [];
|
|
9629
|
-
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
9630
|
-
const s = slug(rawPath);
|
|
9631
|
-
return [
|
|
9632
|
-
{
|
|
9633
|
-
name: `${verdict}-path-${s}-bash`,
|
|
9634
|
-
tool: "bash",
|
|
9635
|
-
conditions: [{ field: "command", op: "matches", value }],
|
|
9636
|
-
verdict,
|
|
9637
|
-
reason: why
|
|
9638
|
-
},
|
|
9639
|
-
// Keep the historical `-anytool` name for the file_path rule: the
|
|
9640
|
-
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
9641
|
-
{
|
|
9642
|
-
name: `${verdict}-path-${s}-anytool`,
|
|
9643
|
-
tool: "*",
|
|
9644
|
-
conditions: [{ field: "file_path", op: "matches", value }],
|
|
9645
|
-
verdict,
|
|
9646
|
-
reason: why
|
|
9647
|
-
},
|
|
9648
|
-
{
|
|
9649
|
-
name: `${verdict}-path-${s}-anytool-path`,
|
|
9650
|
-
tool: "*",
|
|
9651
|
-
conditions: [{ field: "path", op: "matches", value }],
|
|
9652
|
-
verdict,
|
|
9653
|
-
reason: why
|
|
9654
|
-
},
|
|
9655
|
-
{
|
|
9656
|
-
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
9657
|
-
tool: "*",
|
|
9658
|
-
conditions: [{ field: "pattern", op: "matches", value }],
|
|
9659
|
-
verdict,
|
|
9660
|
-
reason: why
|
|
9661
|
-
}
|
|
9662
|
-
];
|
|
9663
|
-
}
|
|
9664
|
-
var B, SEP;
|
|
9665
|
-
var init_build = __esm({
|
|
9666
|
-
"src/shields/build.ts"() {
|
|
9667
|
-
"use strict";
|
|
9668
|
-
init_dist();
|
|
9669
|
-
B = "[\\s/\\\\]";
|
|
9670
|
-
SEP = "[/\\\\]";
|
|
9671
|
-
}
|
|
9672
|
-
});
|
|
9673
|
-
|
|
9674
10495
|
// src/auth/trusted-hosts.ts
|
|
9675
10496
|
function normalizeHost2(raw) {
|
|
9676
10497
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
@@ -10029,6 +10850,7 @@ function getConfig(cwd) {
|
|
|
10029
10850
|
trustedHosts: [],
|
|
10030
10851
|
trustedHostsManaged: false,
|
|
10031
10852
|
appPermissions: {},
|
|
10853
|
+
appLinks: {},
|
|
10032
10854
|
managedJailPaths: []
|
|
10033
10855
|
};
|
|
10034
10856
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
@@ -10426,6 +11248,13 @@ function getConfig(cwd) {
|
|
|
10426
11248
|
mergedPolicy.trustedHostsManaged = true;
|
|
10427
11249
|
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
10428
11250
|
}
|
|
11251
|
+
if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
|
|
11252
|
+
const links = {};
|
|
11253
|
+
for (const [key, slug2] of Object.entries(mc.appLinks)) {
|
|
11254
|
+
if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
|
|
11255
|
+
}
|
|
11256
|
+
mergedPolicy.appLinks = links;
|
|
11257
|
+
}
|
|
10429
11258
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
10430
11259
|
const coerced = {};
|
|
10431
11260
|
for (const [srv, tools] of Object.entries(mc.appPermissions)) {
|
|
@@ -10726,6 +11555,7 @@ var init_config = __esm({
|
|
|
10726
11555
|
init_build();
|
|
10727
11556
|
init_trusted_hosts();
|
|
10728
11557
|
init_dist();
|
|
11558
|
+
init_dist();
|
|
10729
11559
|
init_v2();
|
|
10730
11560
|
init_dist();
|
|
10731
11561
|
init_dist();
|
|
@@ -10859,7 +11689,9 @@ var init_config = __esm({
|
|
|
10859
11689
|
op: "matches",
|
|
10860
11690
|
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
10861
11691
|
// fails the engine's ReDoS check and the rule would be dropped.
|
|
10862
|
-
|
|
11692
|
+
// `apps`: turning an app off, allowing one of its actions, or
|
|
11693
|
+
// linking a server to an app (the general checks leave it).
|
|
11694
|
+
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",
|
|
10863
11695
|
flags: "i"
|
|
10864
11696
|
}
|
|
10865
11697
|
],
|
|
@@ -10945,7 +11777,12 @@ var init_config = __esm({
|
|
|
10945
11777
|
verdict: "block",
|
|
10946
11778
|
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
10947
11779
|
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."
|
|
10948
|
-
}
|
|
11780
|
+
},
|
|
11781
|
+
// ── Actions in apps (node9Firewall#390) ───────────────────────────────
|
|
11782
|
+
// actions.money / actions.access / actions.publish: the action templates
|
|
11783
|
+
// on every MCP server no app shield claims, plus the shell publishing
|
|
11784
|
+
// rules. Review by default; the check value governs them.
|
|
11785
|
+
...GENERAL_ACTION_RULES
|
|
10949
11786
|
],
|
|
10950
11787
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
|
|
10951
11788
|
egress: {
|
|
@@ -10982,6 +11819,7 @@ var init_config = __esm({
|
|
|
10982
11819
|
trustedHosts: [],
|
|
10983
11820
|
trustedHostsManaged: false,
|
|
10984
11821
|
appPermissions: {},
|
|
11822
|
+
appLinks: {},
|
|
10985
11823
|
managedJailPaths: []
|
|
10986
11824
|
},
|
|
10987
11825
|
environments: {}
|
|
@@ -13532,7 +14370,7 @@ function loadShieldStatus() {
|
|
|
13532
14370
|
cfg.policySource === "workspace" ? cfg.policy.appliedShields ?? [] : readActiveShields()
|
|
13533
14371
|
);
|
|
13534
14372
|
const active = all.filter((n) => activeSet.has(n));
|
|
13535
|
-
const inactive = all.filter((n) => !activeSet.has(n) && !
|
|
14373
|
+
const inactive = all.filter((n) => !activeSet.has(n) && !APP_SHIELD_NAMES.has(n));
|
|
13536
14374
|
return { active, inactive };
|
|
13537
14375
|
} catch {
|
|
13538
14376
|
return { active: [], inactive: [] };
|