@carllee1983/dbcli 4.0.0 → 7.0.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +305 -0
- package/dist/cli-runtime.mjs +1826 -1110
- package/dist/cli.mjs +4 -2
- package/dist/core.d.ts +43 -8
- package/dist/core.mjs +760 -191
- package/gemini-extension.json +1 -1
- package/package.json +4 -2
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
package/dist/core.mjs
CHANGED
|
@@ -1323,6 +1323,7 @@ var messages_default = {
|
|
|
1323
1323
|
unsupported_lang: "Language '{lang}' not supported, using English",
|
|
1324
1324
|
invalid_json: "Invalid JSON: {message}",
|
|
1325
1325
|
table_blacklisted: "Error: Table '{table}' is blacklisted for {operation} operations",
|
|
1326
|
+
redis_target_blacklisted: "Redis command refused by blacklist",
|
|
1326
1327
|
column_blacklisted_write: "Error: {operation} on table '{table}' touches blacklisted columns: {columns}",
|
|
1327
1328
|
table_already_blacklisted: "Error: Table '{table}' is already blacklisted",
|
|
1328
1329
|
table_not_in_blacklist: "Error: Table '{table}' is not in the blacklist",
|
|
@@ -1414,7 +1415,8 @@ Reference: {referencePath}`,
|
|
|
1414
1415
|
columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
|
|
1415
1416
|
},
|
|
1416
1417
|
warnings: {
|
|
1417
|
-
blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'"
|
|
1418
|
+
blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'",
|
|
1419
|
+
redis_blacklist_override_active: "Warning: Redis blacklist enforcement disabled by DBCLI_OVERRIDE_BLACKLIST=true"
|
|
1418
1420
|
},
|
|
1419
1421
|
version: {
|
|
1420
1422
|
unsupported_warning: "{system} {version} is below minimum supported version {minVersion}. Some features may not work correctly.",
|
|
@@ -1637,6 +1639,7 @@ var messages_default2 = {
|
|
|
1637
1639
|
unsupported_lang: "\u4E0D\u652F\u63F4\u7684\u8A9E\u8A00 '{lang}'\uFF0C\u4F7F\u7528\u82F1\u6587",
|
|
1638
1640
|
invalid_json: "\u7121\u6548\u7684 JSON\uFF1A{message}",
|
|
1639
1641
|
table_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5DF2\u88AB\u9ED1\u540D\u55AE",
|
|
1642
|
+
redis_target_blacklisted: "Redis \u6307\u4EE4\u5DF2\u88AB\u9ED1\u540D\u55AE\u62D2\u7D55",
|
|
1640
1643
|
column_blacklisted_write: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5305\u542B\u9ED1\u540D\u55AE\u6B04\u4F4D: {columns}",
|
|
1641
1644
|
table_already_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
1642
1645
|
table_not_in_blacklist: "\u932F\u8AA4: \u8868\u683C '{table}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
@@ -1728,7 +1731,8 @@ var messages_default2 = {
|
|
|
1728
1731
|
columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
|
|
1729
1732
|
},
|
|
1730
1733
|
warnings: {
|
|
1731
|
-
blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A"
|
|
1734
|
+
blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A",
|
|
1735
|
+
redis_blacklist_override_active: "\u8B66\u544A: DBCLI_OVERRIDE_BLACKLIST=true \u5DF2\u505C\u7528 Redis \u9ED1\u540D\u55AE\u5F37\u5236\u57F7\u884C"
|
|
1732
1736
|
},
|
|
1733
1737
|
version: {
|
|
1734
1738
|
unsupported_warning: "{system} {version} \u4F4E\u65BC\u6700\u4F4E\u652F\u63F4\u7248\u672C {minVersion}\u3002\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u7121\u6CD5\u6B63\u5E38\u904B\u4F5C\u3002",
|
|
@@ -2024,6 +2028,263 @@ var messageLoader = MessageLoader.getInstance();
|
|
|
2024
2028
|
var t = (key) => messageLoader.t(key);
|
|
2025
2029
|
var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
|
|
2026
2030
|
|
|
2031
|
+
// src/utils/case-fold.ts
|
|
2032
|
+
function foldCase(text) {
|
|
2033
|
+
const lowered = (text.includes("\u0130") ? text.replaceAll("\u0130", "i") : text).toLowerCase();
|
|
2034
|
+
return lowered.includes("\u03C2") ? lowered.replaceAll("\u03C2", "\u03C3") : lowered;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/utils/glob.ts
|
|
2038
|
+
function findClassEnd(glob, open) {
|
|
2039
|
+
for (let i = open + 1;i < glob.length; i++) {
|
|
2040
|
+
if (glob[i] === "\\") {
|
|
2041
|
+
i++;
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
if (glob[i] === "]")
|
|
2045
|
+
return i;
|
|
2046
|
+
}
|
|
2047
|
+
return -1;
|
|
2048
|
+
}
|
|
2049
|
+
function isValidCharacterClass(body) {
|
|
2050
|
+
try {
|
|
2051
|
+
new RegExp(body);
|
|
2052
|
+
return true;
|
|
2053
|
+
} catch {
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
var parsedGlobs = new Map;
|
|
2058
|
+
var PARSED_GLOB_LIMIT = 4096;
|
|
2059
|
+
function parseGlob(glob, caseInsensitive) {
|
|
2060
|
+
const key = `${caseInsensitive ? "i" : "c"}\x00${glob}`;
|
|
2061
|
+
const memo = parsedGlobs.get(key);
|
|
2062
|
+
if (memo !== undefined)
|
|
2063
|
+
return memo;
|
|
2064
|
+
const parsed = parseGlobUncached(glob, caseInsensitive);
|
|
2065
|
+
if (parsedGlobs.size >= PARSED_GLOB_LIMIT)
|
|
2066
|
+
parsedGlobs.clear();
|
|
2067
|
+
parsedGlobs.set(key, parsed);
|
|
2068
|
+
return parsed;
|
|
2069
|
+
}
|
|
2070
|
+
function parseGlobUncached(glob, caseInsensitive) {
|
|
2071
|
+
const runs = [];
|
|
2072
|
+
let current = [];
|
|
2073
|
+
let leadingStar = false;
|
|
2074
|
+
let trailingStar = false;
|
|
2075
|
+
let sawStar = false;
|
|
2076
|
+
const endRun = () => {
|
|
2077
|
+
if (current.length > 0)
|
|
2078
|
+
runs.push(current);
|
|
2079
|
+
current = [];
|
|
2080
|
+
};
|
|
2081
|
+
for (let i = 0;i < glob.length; i++) {
|
|
2082
|
+
const c = glob[i];
|
|
2083
|
+
if (c === "\\" && i + 1 < glob.length) {
|
|
2084
|
+
trailingStar = false;
|
|
2085
|
+
const escaped = String.fromCodePoint(glob.codePointAt(++i));
|
|
2086
|
+
i += escaped.length - 1;
|
|
2087
|
+
current.push(...literalToken(escaped, caseInsensitive));
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (c === "*") {
|
|
2091
|
+
if (!sawStar && current.length === 0)
|
|
2092
|
+
leadingStar = true;
|
|
2093
|
+
sawStar = true;
|
|
2094
|
+
trailingStar = true;
|
|
2095
|
+
endRun();
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
trailingStar = false;
|
|
2099
|
+
if (c === "?") {
|
|
2100
|
+
current.push({ kind: "any" });
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (c === "[") {
|
|
2104
|
+
const end = findClassEnd(glob, i);
|
|
2105
|
+
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
2106
|
+
if (body !== "" && isValidCharacterClass(body)) {
|
|
2107
|
+
current.push({
|
|
2108
|
+
kind: "class",
|
|
2109
|
+
test: new RegExp(`^${body}$`, caseInsensitive ? "si" : "s")
|
|
2110
|
+
});
|
|
2111
|
+
i = end;
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
current.push(...literalToken("[", caseInsensitive));
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const literal = String.fromCodePoint(glob.codePointAt(i));
|
|
2118
|
+
i += literal.length - 1;
|
|
2119
|
+
current.push(...literalToken(literal, caseInsensitive));
|
|
2120
|
+
}
|
|
2121
|
+
endRun();
|
|
2122
|
+
return { runs, leadingStar, trailingStar, anchored: !sawStar };
|
|
2123
|
+
}
|
|
2124
|
+
function literalToken(codePoint, caseInsensitive) {
|
|
2125
|
+
const folded = caseInsensitive ? foldCase(codePoint) : codePoint;
|
|
2126
|
+
return folded.split("").map((unit) => ({ kind: "literal", char: unit }));
|
|
2127
|
+
}
|
|
2128
|
+
function runMatchesAt(run, text, raw, at) {
|
|
2129
|
+
if (at + run.length > text.length)
|
|
2130
|
+
return false;
|
|
2131
|
+
for (let i = 0;i < run.length; i++) {
|
|
2132
|
+
const token = run[i];
|
|
2133
|
+
if (token.kind === "literal") {
|
|
2134
|
+
if (token.char !== text[at + i])
|
|
2135
|
+
return false;
|
|
2136
|
+
} else if (token.kind === "class") {
|
|
2137
|
+
const rawChar = raw[at + i];
|
|
2138
|
+
if (rawChar === undefined) {
|
|
2139
|
+
throw new Error("globMatches: folded and unfolded subject lengths disagree");
|
|
2140
|
+
}
|
|
2141
|
+
if (!token.test.test(rawChar))
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
function globMatches(glob, subject, options) {
|
|
2148
|
+
const caseInsensitive = options?.caseInsensitive === true;
|
|
2149
|
+
const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob, caseInsensitive);
|
|
2150
|
+
const text = caseInsensitive ? foldCase(subject) : subject;
|
|
2151
|
+
if (anchored) {
|
|
2152
|
+
const run = runs[0] ?? [];
|
|
2153
|
+
return text.length === run.length && runMatchesAt(run, text, subject, 0);
|
|
2154
|
+
}
|
|
2155
|
+
if (runs.length === 0)
|
|
2156
|
+
return true;
|
|
2157
|
+
let cursor = 0;
|
|
2158
|
+
let first = 0;
|
|
2159
|
+
let last = runs.length;
|
|
2160
|
+
if (!leadingStar) {
|
|
2161
|
+
const head = runs[0];
|
|
2162
|
+
if (!runMatchesAt(head, text, subject, 0))
|
|
2163
|
+
return false;
|
|
2164
|
+
cursor = head.length;
|
|
2165
|
+
first = 1;
|
|
2166
|
+
}
|
|
2167
|
+
if (!trailingStar) {
|
|
2168
|
+
const tail = runs[last - 1];
|
|
2169
|
+
const at = text.length - tail.length;
|
|
2170
|
+
if (at < cursor || !runMatchesAt(tail, text, subject, at))
|
|
2171
|
+
return false;
|
|
2172
|
+
last -= 1;
|
|
2173
|
+
}
|
|
2174
|
+
const limit = trailingStar ? text.length : text.length - runs[runs.length - 1].length;
|
|
2175
|
+
for (let r = first;r < last; r++) {
|
|
2176
|
+
const run = runs[r];
|
|
2177
|
+
let found = -1;
|
|
2178
|
+
for (let at = cursor;at + run.length <= limit; at++) {
|
|
2179
|
+
if (runMatchesAt(run, text, subject, at)) {
|
|
2180
|
+
found = at;
|
|
2181
|
+
break;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (found === -1)
|
|
2185
|
+
return false;
|
|
2186
|
+
cursor = found + run.length;
|
|
2187
|
+
}
|
|
2188
|
+
return true;
|
|
2189
|
+
}
|
|
2190
|
+
function isGlobPattern(value) {
|
|
2191
|
+
return /[*?[\\]/.test(value);
|
|
2192
|
+
}
|
|
2193
|
+
function globNeverMatches(glob) {
|
|
2194
|
+
for (let i = 0;i < glob.length; i++) {
|
|
2195
|
+
const c = glob[i];
|
|
2196
|
+
if (c === "\\") {
|
|
2197
|
+
i++;
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (c !== "[")
|
|
2201
|
+
continue;
|
|
2202
|
+
const end = findClassEnd(glob, i);
|
|
2203
|
+
if (end === -1) {
|
|
2204
|
+
return "unclosed character class";
|
|
2205
|
+
}
|
|
2206
|
+
const body = glob.slice(i, end + 1);
|
|
2207
|
+
if (body === "[]" || body === "[^]")
|
|
2208
|
+
return "empty character class matches nothing";
|
|
2209
|
+
if (!isValidCharacterClass(body))
|
|
2210
|
+
return "invalid character class";
|
|
2211
|
+
i = end;
|
|
2212
|
+
}
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/core/blacklist-fold.ts
|
|
2217
|
+
function foldFieldPath(path) {
|
|
2218
|
+
return foldCase(path);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// src/core/mongo/path-matcher.ts
|
|
2222
|
+
function compilePatterns(raw) {
|
|
2223
|
+
const patterns = [];
|
|
2224
|
+
const rejected = [];
|
|
2225
|
+
for (const entry of raw) {
|
|
2226
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
2227
|
+
rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const segments = entry.split(".");
|
|
2231
|
+
if (segments.some((s) => s.length === 0)) {
|
|
2232
|
+
rejected.push({ raw: entry, reason: "empty path segment" });
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
const globstar = segments.find((seg) => seg.includes("**"));
|
|
2236
|
+
if (globstar !== undefined) {
|
|
2237
|
+
rejected.push({
|
|
2238
|
+
raw: entry,
|
|
2239
|
+
reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
|
|
2240
|
+
});
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
|
|
2244
|
+
if (dead !== undefined && dead !== null) {
|
|
2245
|
+
rejected.push({ raw: entry, reason: dead });
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
|
|
2249
|
+
const literal = wildcardTail ? segments.slice(0, -1) : segments;
|
|
2250
|
+
patterns.push({
|
|
2251
|
+
raw: entry,
|
|
2252
|
+
segments: literal,
|
|
2253
|
+
wildcardTail
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
return { patterns, rejected };
|
|
2257
|
+
}
|
|
2258
|
+
var FOLD_CASE = { caseInsensitive: true };
|
|
2259
|
+
function matchSegment(pattern, segment) {
|
|
2260
|
+
return globMatches(pattern, segment, FOLD_CASE);
|
|
2261
|
+
}
|
|
2262
|
+
function matchAny(path, patterns) {
|
|
2263
|
+
if (patterns.length === 0)
|
|
2264
|
+
return false;
|
|
2265
|
+
const pathSegments = path.split(".");
|
|
2266
|
+
for (const pat of patterns) {
|
|
2267
|
+
if (pat.wildcardTail) {
|
|
2268
|
+
if (pathSegments.length < pat.segments.length)
|
|
2269
|
+
continue;
|
|
2270
|
+
} else {
|
|
2271
|
+
if (pat.segments.length !== pathSegments.length)
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
let ok = true;
|
|
2275
|
+
for (let i = 0;i < pat.segments.length; i++) {
|
|
2276
|
+
if (!globMatches(pat.segments[i], pathSegments[i], FOLD_CASE)) {
|
|
2277
|
+
ok = false;
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (ok)
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
var compiledRuleSets = new WeakMap;
|
|
2287
|
+
|
|
2027
2288
|
// src/core/mongo/request-fields.ts
|
|
2028
2289
|
var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
|
|
2029
2290
|
var RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
|
|
@@ -2468,62 +2729,180 @@ var REDIS_COMMAND_TABLE = Object.freeze({
|
|
|
2468
2729
|
})
|
|
2469
2730
|
});
|
|
2470
2731
|
|
|
2471
|
-
// src/
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2732
|
+
// src/adapters/redis/returned-key-names.ts
|
|
2733
|
+
var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
2734
|
+
|
|
2735
|
+
// src/adapters/redis/value-masker.ts
|
|
2736
|
+
var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
|
|
2737
|
+
|
|
2738
|
+
// src/core/blacklist-manager.ts
|
|
2739
|
+
function isBlacklistOverrideEnabled(value) {
|
|
2740
|
+
return (value ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
|
|
2741
|
+
}
|
|
2742
|
+
function normalizeBlacklistEntry(raw) {
|
|
2743
|
+
const trimmed = raw.trim();
|
|
2744
|
+
const quote = trimmed[0];
|
|
2745
|
+
if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
|
|
2746
|
+
return trimmed.slice(1, -1).trim();
|
|
2747
|
+
}
|
|
2748
|
+
return trimmed;
|
|
2749
|
+
}
|
|
2750
|
+
function assertNotTableQualified(tableKey, column) {
|
|
2751
|
+
const dot = column.indexOf(".");
|
|
2752
|
+
if (dot <= 0)
|
|
2753
|
+
return;
|
|
2754
|
+
if (foldFieldPath(column.slice(0, dot)) !== foldFieldPath(tableKey))
|
|
2755
|
+
return;
|
|
2756
|
+
throw new Error(`[BlacklistManager] blacklist.columns["${tableKey}"] entry ${JSON.stringify(column)} is ` + `qualified with its own table and would never match. Write it as ` + `${JSON.stringify(column.slice(dot + 1))}.`);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
class BlacklistManager {
|
|
2760
|
+
config;
|
|
2761
|
+
state;
|
|
2762
|
+
overrideEnabled;
|
|
2763
|
+
rawTables = [];
|
|
2764
|
+
constructor(config, overrideEnvValue) {
|
|
2765
|
+
this.config = config;
|
|
2766
|
+
this.overrideEnabled = isBlacklistOverrideEnabled(overrideEnvValue);
|
|
2767
|
+
this.state = this.loadBlacklist();
|
|
2768
|
+
this.wildcardTables = this.rawTables.filter(isGlobPattern);
|
|
2769
|
+
}
|
|
2770
|
+
loadBlacklist() {
|
|
2771
|
+
this.rawTables.length = 0;
|
|
2772
|
+
const tables = new Set;
|
|
2773
|
+
const columns = new Map;
|
|
2774
|
+
const blacklistConfig = this.config.blacklist;
|
|
2775
|
+
if (!blacklistConfig) {
|
|
2776
|
+
return { tables, columns };
|
|
2479
2777
|
}
|
|
2480
|
-
if (
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
i = end;
|
|
2490
|
-
} else {
|
|
2491
|
-
out += "\\[";
|
|
2778
|
+
if (Array.isArray(blacklistConfig.tables)) {
|
|
2779
|
+
for (const tableName of blacklistConfig.tables) {
|
|
2780
|
+
if (typeof tableName === "string") {
|
|
2781
|
+
const entry = normalizeBlacklistEntry(tableName);
|
|
2782
|
+
this.rawTables.push(entry);
|
|
2783
|
+
tables.add(foldFieldPath(entry));
|
|
2784
|
+
} else {
|
|
2785
|
+
console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
|
|
2786
|
+
}
|
|
2492
2787
|
}
|
|
2493
|
-
} else if (
|
|
2494
|
-
|
|
2495
|
-
} else {
|
|
2496
|
-
out += c;
|
|
2788
|
+
} else if (blacklistConfig.tables !== undefined) {
|
|
2789
|
+
console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
|
|
2497
2790
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2791
|
+
if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
|
|
2792
|
+
for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
|
|
2793
|
+
if (typeof tableName !== "string") {
|
|
2794
|
+
console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
if (!Array.isArray(cols)) {
|
|
2798
|
+
console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
const columnSet = new Set;
|
|
2802
|
+
for (const col of cols) {
|
|
2803
|
+
if (typeof col === "string") {
|
|
2804
|
+
const entry = normalizeBlacklistEntry(col);
|
|
2805
|
+
assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
|
|
2806
|
+
columnSet.add(entry);
|
|
2807
|
+
} else {
|
|
2808
|
+
console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
if (columnSet.size > 0) {
|
|
2812
|
+
columns.set(foldFieldPath(normalizeBlacklistEntry(tableName)), columnSet);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
} else if (blacklistConfig.columns !== undefined) {
|
|
2816
|
+
console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
|
|
2507
2817
|
}
|
|
2508
|
-
|
|
2509
|
-
return i;
|
|
2818
|
+
return { tables, columns };
|
|
2510
2819
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2820
|
+
isTableBlacklisted(tableName) {
|
|
2821
|
+
const name = foldFieldPath(tableName);
|
|
2822
|
+
if (this.state.tables.has(name))
|
|
2823
|
+
return true;
|
|
2824
|
+
for (const pattern of this.wildcardTables) {
|
|
2825
|
+
if (globMatches(pattern, tableName, { caseInsensitive: true }))
|
|
2826
|
+
return true;
|
|
2827
|
+
}
|
|
2518
2828
|
return false;
|
|
2519
2829
|
}
|
|
2830
|
+
wildcardTables;
|
|
2831
|
+
columnRulesFor(tableName) {
|
|
2832
|
+
const key = foldFieldPath(normalizeBlacklistEntry(tableName));
|
|
2833
|
+
const direct = this.state.columns.get(key);
|
|
2834
|
+
if (direct)
|
|
2835
|
+
return direct;
|
|
2836
|
+
const dot = key.lastIndexOf(".");
|
|
2837
|
+
if (dot > 0) {
|
|
2838
|
+
const bare = this.state.columns.get(key.slice(dot + 1));
|
|
2839
|
+
if (bare)
|
|
2840
|
+
return bare;
|
|
2841
|
+
}
|
|
2842
|
+
let merged;
|
|
2843
|
+
for (const [ruleKey, columns] of this.state.columns) {
|
|
2844
|
+
const ruleDot = ruleKey.lastIndexOf(".");
|
|
2845
|
+
if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
|
|
2846
|
+
continue;
|
|
2847
|
+
merged ??= new Set;
|
|
2848
|
+
for (const column of columns)
|
|
2849
|
+
merged.add(column);
|
|
2850
|
+
}
|
|
2851
|
+
return merged;
|
|
2852
|
+
}
|
|
2853
|
+
isColumnBlacklisted(tableName, columnName) {
|
|
2854
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
2855
|
+
if (!columnSet) {
|
|
2856
|
+
return false;
|
|
2857
|
+
}
|
|
2858
|
+
const target = foldFieldPath(normalizeBlacklistEntry(columnName));
|
|
2859
|
+
const compiled = this.foldedColumnsFor(columnSet);
|
|
2860
|
+
if (compiled.literals.has(target))
|
|
2861
|
+
return true;
|
|
2862
|
+
return compiled.globs.length > 0 && matchAny(target, compiled.globs);
|
|
2863
|
+
}
|
|
2864
|
+
foldedColumns = new WeakMap;
|
|
2865
|
+
foldedColumnsFor(columnSet) {
|
|
2866
|
+
const memo = this.foldedColumns.get(columnSet);
|
|
2867
|
+
if (memo !== undefined)
|
|
2868
|
+
return memo;
|
|
2869
|
+
const literals = new Set;
|
|
2870
|
+
const globbed = [];
|
|
2871
|
+
for (const rule of columnSet) {
|
|
2872
|
+
if (isGlobPattern(rule))
|
|
2873
|
+
globbed.push(rule);
|
|
2874
|
+
else
|
|
2875
|
+
literals.add(foldFieldPath(rule));
|
|
2876
|
+
}
|
|
2877
|
+
const folded = {
|
|
2878
|
+
literals,
|
|
2879
|
+
globs: globbed.length > 0 ? compilePatterns(globbed).patterns : []
|
|
2880
|
+
};
|
|
2881
|
+
this.foldedColumns.set(columnSet, folded);
|
|
2882
|
+
return folded;
|
|
2883
|
+
}
|
|
2884
|
+
getBlacklistedColumns(tableName) {
|
|
2885
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
2886
|
+
if (!columnSet) {
|
|
2887
|
+
return [];
|
|
2888
|
+
}
|
|
2889
|
+
return Array.from(columnSet);
|
|
2890
|
+
}
|
|
2891
|
+
getAllBlacklistedColumns() {
|
|
2892
|
+
const all = new Set;
|
|
2893
|
+
for (const columnSet of this.state.columns.values()) {
|
|
2894
|
+
for (const column of columnSet)
|
|
2895
|
+
all.add(column);
|
|
2896
|
+
}
|
|
2897
|
+
return Array.from(all);
|
|
2898
|
+
}
|
|
2899
|
+
canOverrideBlacklist() {
|
|
2900
|
+
return this.overrideEnabled;
|
|
2901
|
+
}
|
|
2902
|
+
getState() {
|
|
2903
|
+
return this.state;
|
|
2904
|
+
}
|
|
2520
2905
|
}
|
|
2521
|
-
|
|
2522
|
-
// src/adapters/redis/returned-key-names.ts
|
|
2523
|
-
var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
2524
|
-
|
|
2525
|
-
// src/adapters/redis/value-masker.ts
|
|
2526
|
-
var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
|
|
2527
2906
|
// src/utils/sql-lexical.ts
|
|
2528
2907
|
var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
|
|
2529
2908
|
var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
|
|
@@ -3444,18 +3823,139 @@ function projectRows(rows, selection) {
|
|
|
3444
3823
|
const columnNames = collectColumnNames(projectedRows);
|
|
3445
3824
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
3446
3825
|
}
|
|
3447
|
-
function hasFieldPath(row, segments) {
|
|
3448
|
-
|
|
3826
|
+
function hasFieldPath(row, segments, options) {
|
|
3827
|
+
const caseInsensitive = options?.caseInsensitive === true;
|
|
3828
|
+
const folded = caseInsensitive && options?.alreadyFolded !== true ? segments.map(foldCase) : segments;
|
|
3829
|
+
return readPath(row, folded, caseInsensitive).found;
|
|
3830
|
+
}
|
|
3831
|
+
function narrow(rules, key) {
|
|
3832
|
+
const parts = key.includes(".") ? key.split(".") : [key];
|
|
3833
|
+
const live = [];
|
|
3834
|
+
for (const rule of rules) {
|
|
3835
|
+
const remaining = rule.segments.length;
|
|
3836
|
+
if (remaining < parts.length && !rule.wildcardTail)
|
|
3837
|
+
continue;
|
|
3838
|
+
let matches = true;
|
|
3839
|
+
for (let i = 0;i < Math.min(remaining, parts.length); i++) {
|
|
3840
|
+
if (!matchSegment(rule.segments[i], parts[i])) {
|
|
3841
|
+
matches = false;
|
|
3842
|
+
break;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
if (!matches)
|
|
3846
|
+
continue;
|
|
3847
|
+
if (remaining <= parts.length)
|
|
3848
|
+
return { drop: true, live: [] };
|
|
3849
|
+
live.push({
|
|
3850
|
+
root: rule.root,
|
|
3851
|
+
rootIndex: rule.rootIndex,
|
|
3852
|
+
offset: rule.offset + parts.length,
|
|
3853
|
+
segments: rule.segments.slice(parts.length),
|
|
3854
|
+
wildcardTail: rule.wildcardTail
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
return { drop: false, live };
|
|
3858
|
+
}
|
|
3859
|
+
function liveRulesOf(patterns) {
|
|
3860
|
+
return patterns.map((pattern, index) => ({
|
|
3861
|
+
root: pattern,
|
|
3862
|
+
rootIndex: index,
|
|
3863
|
+
offset: 0,
|
|
3864
|
+
segments: pattern.segments,
|
|
3865
|
+
wildcardTail: pattern.wildcardTail
|
|
3866
|
+
}));
|
|
3449
3867
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3868
|
+
var NARROW_STATE_LIMIT = 4096;
|
|
3869
|
+
|
|
3870
|
+
class NarrowStates {
|
|
3871
|
+
states = [];
|
|
3872
|
+
ids = new Map;
|
|
3873
|
+
transitions = new Map;
|
|
3874
|
+
constructor(rules) {
|
|
3875
|
+
this.intern(rules);
|
|
3876
|
+
}
|
|
3877
|
+
rules(state) {
|
|
3878
|
+
const rules = this.states[state];
|
|
3879
|
+
if (rules === undefined)
|
|
3880
|
+
throw new Error(`unknown nested-rule state ${state}`);
|
|
3881
|
+
return rules;
|
|
3882
|
+
}
|
|
3883
|
+
step(state, key) {
|
|
3884
|
+
const memoKey = `${state}\x00${key}`;
|
|
3885
|
+
const memo = this.transitions.get(memoKey);
|
|
3886
|
+
if (memo !== undefined)
|
|
3887
|
+
return memo;
|
|
3888
|
+
const { drop, live } = narrow(this.rules(state), key);
|
|
3889
|
+
const step = drop ? { drop: true } : { drop: false, next: this.intern(live) };
|
|
3890
|
+
if (this.transitions.size >= NARROW_STATE_LIMIT)
|
|
3891
|
+
this.transitions = new Map;
|
|
3892
|
+
this.transitions.set(memoKey, step);
|
|
3893
|
+
return step;
|
|
3894
|
+
}
|
|
3895
|
+
intern(rules) {
|
|
3896
|
+
const key = rules.map((rule) => `${rule.rootIndex}:${rule.offset}`).join(",");
|
|
3897
|
+
const existing = this.ids.get(key);
|
|
3898
|
+
if (existing !== undefined)
|
|
3899
|
+
return existing;
|
|
3900
|
+
const id = this.states.length;
|
|
3901
|
+
this.states.push(rules);
|
|
3902
|
+
this.ids.set(key, id);
|
|
3903
|
+
return id;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
function collectNestedMatches(row, states, pending, onMatch) {
|
|
3907
|
+
if (pending.size === 0)
|
|
3908
|
+
return;
|
|
3909
|
+
const walk = (value, state, depth) => {
|
|
3910
|
+
if (pending.size === 0 || states.rules(state).length === 0)
|
|
3911
|
+
return;
|
|
3912
|
+
if (Array.isArray(value)) {
|
|
3913
|
+
for (const item of value)
|
|
3914
|
+
walk(item, state, depth);
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
if (!isPlainRecord(value))
|
|
3918
|
+
return;
|
|
3919
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
3920
|
+
const step = states.step(state, key);
|
|
3921
|
+
if (step.drop) {
|
|
3922
|
+
if (depth > 0) {
|
|
3923
|
+
for (const rule of states.rules(state)) {
|
|
3924
|
+
if (!pending.has(rule.root))
|
|
3925
|
+
continue;
|
|
3926
|
+
if (!narrow([rule], key).drop)
|
|
3927
|
+
continue;
|
|
3928
|
+
pending.delete(rule.root);
|
|
3929
|
+
onMatch(rule.root);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3934
|
+
walk(value[key], step.next, depth + 1);
|
|
3935
|
+
}
|
|
3936
|
+
};
|
|
3937
|
+
walk(row, 0, 0);
|
|
3938
|
+
}
|
|
3939
|
+
function compileNestedRules(patterns) {
|
|
3940
|
+
return new NarrowStates(liveRulesOf(patterns));
|
|
3941
|
+
}
|
|
3942
|
+
function omitFieldPaths(rows, paths, options) {
|
|
3943
|
+
const caseInsensitive = options?.caseInsensitive === true;
|
|
3944
|
+
const nestedPatterns = options?.patterns ?? [];
|
|
3945
|
+
const nestedStates = nestedPatterns.length > 0 ? compileNestedRules(nestedPatterns) : undefined;
|
|
3946
|
+
const fold = (value) => caseInsensitive ? foldCase(value) : value;
|
|
3947
|
+
const topLevel = new Set(paths.map(fold));
|
|
3948
|
+
const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: fold(path.slice(0, path.indexOf("."))), segments: path.split(".") }));
|
|
3453
3949
|
const maskRecord = (row) => {
|
|
3454
|
-
let projected = cloneRecord(row, topLevel);
|
|
3950
|
+
let projected = cloneRecord(row, topLevel, caseInsensitive);
|
|
3455
3951
|
for (const { head, segments } of dotted) {
|
|
3456
|
-
const value = row
|
|
3457
|
-
if (value !== null && typeof value === "object")
|
|
3458
|
-
projected = omitPath(projected, segments);
|
|
3952
|
+
const value = headValue(row, head, caseInsensitive);
|
|
3953
|
+
if (value !== null && typeof value === "object") {
|
|
3954
|
+
projected = omitPath(projected, segments, caseInsensitive);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
if (nestedStates !== undefined && hasNestedRecord(row)) {
|
|
3958
|
+
projected = omitMatching(projected, nestedStates, 0);
|
|
3459
3959
|
}
|
|
3460
3960
|
return projected;
|
|
3461
3961
|
};
|
|
@@ -3475,11 +3975,38 @@ function maskArrayRow(row, maskRecord) {
|
|
|
3475
3975
|
});
|
|
3476
3976
|
return out;
|
|
3477
3977
|
}
|
|
3478
|
-
function
|
|
3978
|
+
function headValue(row, head, caseInsensitive) {
|
|
3979
|
+
if (!caseInsensitive)
|
|
3980
|
+
return row[head];
|
|
3981
|
+
const key = foldedKeys(row).get(head);
|
|
3982
|
+
return key === undefined ? undefined : row[key];
|
|
3983
|
+
}
|
|
3984
|
+
var foldedKeyIndex = new WeakMap;
|
|
3985
|
+
function foldedKeys(value) {
|
|
3986
|
+
const memo = foldedKeyIndex.get(value);
|
|
3987
|
+
if (memo !== undefined)
|
|
3988
|
+
return memo;
|
|
3989
|
+
const index = new Map;
|
|
3990
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
3991
|
+
const folded = foldCase(key);
|
|
3992
|
+
if (!index.has(folded))
|
|
3993
|
+
index.set(folded, key);
|
|
3994
|
+
}
|
|
3995
|
+
foldedKeyIndex.set(value, index);
|
|
3996
|
+
return index;
|
|
3997
|
+
}
|
|
3998
|
+
function ownKey(value, name, caseInsensitive) {
|
|
3999
|
+
if (Object.prototype.hasOwnProperty.call(value, name))
|
|
4000
|
+
return name;
|
|
4001
|
+
if (!caseInsensitive)
|
|
4002
|
+
return;
|
|
4003
|
+
return foldedKeys(value).get(name);
|
|
4004
|
+
}
|
|
4005
|
+
function readPath(value, segments, caseInsensitive = false) {
|
|
3479
4006
|
if (segments.length === 0)
|
|
3480
4007
|
return { found: true, value };
|
|
3481
4008
|
if (Array.isArray(value)) {
|
|
3482
|
-
const results = value.map((item) => readPath(item, segments));
|
|
4009
|
+
const results = value.map((item) => readPath(item, segments, caseInsensitive));
|
|
3483
4010
|
return {
|
|
3484
4011
|
found: results.some((result) => result.found),
|
|
3485
4012
|
value: results.map((result) => result.found ? result.value : undefined)
|
|
@@ -3487,41 +4014,68 @@ function readPath(value, segments) {
|
|
|
3487
4014
|
}
|
|
3488
4015
|
if (!isRecord(value))
|
|
3489
4016
|
return { found: false };
|
|
3490
|
-
const
|
|
3491
|
-
if (
|
|
3492
|
-
return { found: true, value: value[
|
|
3493
|
-
}
|
|
4017
|
+
const exact = ownKey(value, segments.join("."), caseInsensitive);
|
|
4018
|
+
if (exact !== undefined)
|
|
4019
|
+
return { found: true, value: value[exact] };
|
|
3494
4020
|
const [head, ...tail] = segments;
|
|
3495
|
-
|
|
4021
|
+
const headKey = ownKey(value, head, caseInsensitive);
|
|
4022
|
+
if (headKey === undefined)
|
|
3496
4023
|
return { found: false };
|
|
3497
|
-
return readPath(value[
|
|
4024
|
+
return readPath(value[headKey], tail, caseInsensitive);
|
|
4025
|
+
}
|
|
4026
|
+
function omitMatching(value, states, state) {
|
|
4027
|
+
if (states.rules(state).length === 0)
|
|
4028
|
+
return value;
|
|
4029
|
+
if (Array.isArray(value))
|
|
4030
|
+
return value.map((item) => omitMatching(item, states, state));
|
|
4031
|
+
if (!isPlainRecord(value))
|
|
4032
|
+
return value;
|
|
4033
|
+
const out = {};
|
|
4034
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4035
|
+
const step = states.step(state, key);
|
|
4036
|
+
if (step.drop)
|
|
4037
|
+
continue;
|
|
4038
|
+
defineData(out, key, omitMatching(child, states, step.next));
|
|
4039
|
+
}
|
|
4040
|
+
return out;
|
|
3498
4041
|
}
|
|
3499
|
-
function
|
|
4042
|
+
function hasNestedRecord(row) {
|
|
4043
|
+
for (const key of Object.getOwnPropertyNames(row)) {
|
|
4044
|
+
const value = row[key];
|
|
4045
|
+
if (Array.isArray(value) || isPlainRecord(value))
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
function omitPath(value, segments, caseInsensitive) {
|
|
3500
4051
|
if (segments.length === 0)
|
|
3501
4052
|
return value;
|
|
3502
4053
|
if (Array.isArray(value))
|
|
3503
|
-
return value.map((item) => omitPath(item, segments));
|
|
4054
|
+
return value.map((item) => omitPath(item, segments, caseInsensitive));
|
|
3504
4055
|
if (!isPlainRecord(value))
|
|
3505
4056
|
return value;
|
|
3506
|
-
const
|
|
3507
|
-
const
|
|
4057
|
+
const fold = (name) => caseInsensitive ? foldCase(name) : name;
|
|
4058
|
+
const exactPath = fold(segments.join("."));
|
|
4059
|
+
const head = fold(segments[0]);
|
|
4060
|
+
const tail = segments.slice(1);
|
|
3508
4061
|
const out = {};
|
|
3509
4062
|
for (const [key, child] of Object.entries(value)) {
|
|
3510
|
-
|
|
4063
|
+
const folded = fold(key);
|
|
4064
|
+
if (folded === exactPath)
|
|
3511
4065
|
continue;
|
|
3512
|
-
if (
|
|
4066
|
+
if (folded === head) {
|
|
3513
4067
|
if (tail.length > 0)
|
|
3514
|
-
defineData(out, key, omitPath(child, tail));
|
|
4068
|
+
defineData(out, key, omitPath(child, tail, caseInsensitive));
|
|
3515
4069
|
continue;
|
|
3516
4070
|
}
|
|
3517
4071
|
defineData(out, key, child);
|
|
3518
4072
|
}
|
|
3519
4073
|
return out;
|
|
3520
4074
|
}
|
|
3521
|
-
function cloneRecord(value, omittedKeys) {
|
|
4075
|
+
function cloneRecord(value, omittedKeys, caseInsensitive = false) {
|
|
3522
4076
|
const out = {};
|
|
3523
4077
|
for (const [key, child] of Object.entries(value)) {
|
|
3524
|
-
if (!omittedKeys.has(key))
|
|
4078
|
+
if (!omittedKeys.has(caseInsensitive ? foldCase(key) : key))
|
|
3525
4079
|
defineData(out, key, child);
|
|
3526
4080
|
}
|
|
3527
4081
|
return out;
|
|
@@ -3627,12 +4181,12 @@ class QueryExecutor {
|
|
|
3627
4181
|
let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
|
|
3628
4182
|
let filteredRows = rows;
|
|
3629
4183
|
let securityNotification;
|
|
3630
|
-
let
|
|
4184
|
+
let reachesOmitted = () => false;
|
|
3631
4185
|
if (this.blacklistValidator) {
|
|
3632
4186
|
const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
|
|
3633
4187
|
filteredRows = filterResult.filteredRows;
|
|
3634
4188
|
if (filterResult.omittedColumns.length > 0) {
|
|
3635
|
-
|
|
4189
|
+
reachesOmitted = filterResult.reachesOmitted;
|
|
3636
4190
|
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
3637
4191
|
securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
|
|
3638
4192
|
}
|
|
@@ -3640,7 +4194,7 @@ class QueryExecutor {
|
|
|
3640
4194
|
if (options?.fieldSelection) {
|
|
3641
4195
|
const fieldSelection = options.fieldSelection.mode === "include" ? {
|
|
3642
4196
|
mode: "include",
|
|
3643
|
-
paths: options.fieldSelection.paths.filter((path) => !
|
|
4197
|
+
paths: options.fieldSelection.paths.filter((path) => !reachesOmitted(path))
|
|
3644
4198
|
} : options.fieldSelection;
|
|
3645
4199
|
const projection = projectRows(filteredRows, fieldSelection);
|
|
3646
4200
|
filteredRows = projection.rows;
|
|
@@ -7977,10 +8531,11 @@ var RedisMaskRuleSchema = exports_external.object({
|
|
|
7977
8531
|
var RedisConfigSchema = exports_external.object({
|
|
7978
8532
|
mask: exports_external.array(RedisMaskRuleSchema).default([])
|
|
7979
8533
|
}).optional();
|
|
8534
|
+
var DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
|
|
7980
8535
|
var AuditRotationConfigSchema = exports_external.object({
|
|
7981
|
-
max_bytes: exports_external.number().int().positive().default(
|
|
7982
|
-
max_entries: exports_external.number().int().positive().default(
|
|
7983
|
-
}).optional().default({
|
|
8536
|
+
max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
|
|
8537
|
+
max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
|
|
8538
|
+
}).optional().default({ ...DEFAULT_AUDIT_ROTATION });
|
|
7984
8539
|
var AuditConfigSchema = exports_external.object({
|
|
7985
8540
|
enabled: exports_external.boolean().default(true),
|
|
7986
8541
|
strict: exports_external.boolean().default(false),
|
|
@@ -7991,7 +8546,7 @@ var AuditConfigSchema = exports_external.object({
|
|
|
7991
8546
|
}).optional().default({
|
|
7992
8547
|
enabled: true,
|
|
7993
8548
|
strict: false,
|
|
7994
|
-
rotation: {
|
|
8549
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
7995
8550
|
});
|
|
7996
8551
|
var DbcliConfigSchema = exports_external.object({
|
|
7997
8552
|
connection: ConnectionConfigSchema,
|
|
@@ -8509,7 +9064,7 @@ function migrateV1ToV2(v1) {
|
|
|
8509
9064
|
schemas: {},
|
|
8510
9065
|
metadata: v1.metadata ?? { version: "2.0" },
|
|
8511
9066
|
blacklist: v1.blacklist ?? { tables: [], columns: {} },
|
|
8512
|
-
audit: v1.audit ?? { enabled: true, rotation: {
|
|
9067
|
+
audit: v1.audit ?? { enabled: true, rotation: { ...DEFAULT_AUDIT_ROTATION } }
|
|
8513
9068
|
};
|
|
8514
9069
|
}
|
|
8515
9070
|
function upsertConnection(config, input) {
|
|
@@ -8781,7 +9336,7 @@ var DEFAULT_CONFIG = {
|
|
|
8781
9336
|
audit: {
|
|
8782
9337
|
enabled: true,
|
|
8783
9338
|
strict: false,
|
|
8784
|
-
rotation: {
|
|
9339
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
8785
9340
|
}
|
|
8786
9341
|
};
|
|
8787
9342
|
function isEnvReference(value) {
|
|
@@ -9035,93 +9590,6 @@ DBCLI_PASSWORD=${password}
|
|
|
9035
9590
|
}
|
|
9036
9591
|
}
|
|
9037
9592
|
};
|
|
9038
|
-
// src/core/blacklist-manager.ts
|
|
9039
|
-
class BlacklistManager {
|
|
9040
|
-
config;
|
|
9041
|
-
state;
|
|
9042
|
-
overrideEnabled;
|
|
9043
|
-
constructor(config, overrideEnvValue) {
|
|
9044
|
-
this.config = config;
|
|
9045
|
-
this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
|
|
9046
|
-
this.state = this.loadBlacklist();
|
|
9047
|
-
}
|
|
9048
|
-
loadBlacklist() {
|
|
9049
|
-
const tables = new Set;
|
|
9050
|
-
const columns = new Map;
|
|
9051
|
-
const blacklistConfig = this.config.blacklist;
|
|
9052
|
-
if (!blacklistConfig) {
|
|
9053
|
-
return { tables, columns };
|
|
9054
|
-
}
|
|
9055
|
-
if (Array.isArray(blacklistConfig.tables)) {
|
|
9056
|
-
for (const tableName of blacklistConfig.tables) {
|
|
9057
|
-
if (typeof tableName === "string") {
|
|
9058
|
-
tables.add(tableName.toLowerCase());
|
|
9059
|
-
} else {
|
|
9060
|
-
console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
|
|
9061
|
-
}
|
|
9062
|
-
}
|
|
9063
|
-
} else if (blacklistConfig.tables !== undefined) {
|
|
9064
|
-
console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
|
|
9065
|
-
}
|
|
9066
|
-
if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
|
|
9067
|
-
for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
|
|
9068
|
-
if (typeof tableName !== "string") {
|
|
9069
|
-
console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
|
|
9070
|
-
continue;
|
|
9071
|
-
}
|
|
9072
|
-
if (!Array.isArray(cols)) {
|
|
9073
|
-
console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
|
|
9074
|
-
continue;
|
|
9075
|
-
}
|
|
9076
|
-
const columnSet = new Set;
|
|
9077
|
-
for (const col of cols) {
|
|
9078
|
-
if (typeof col === "string") {
|
|
9079
|
-
columnSet.add(col);
|
|
9080
|
-
} else {
|
|
9081
|
-
console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
|
|
9082
|
-
}
|
|
9083
|
-
}
|
|
9084
|
-
if (columnSet.size > 0) {
|
|
9085
|
-
columns.set(tableName.toLowerCase(), columnSet);
|
|
9086
|
-
}
|
|
9087
|
-
}
|
|
9088
|
-
} else if (blacklistConfig.columns !== undefined) {
|
|
9089
|
-
console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
|
|
9090
|
-
}
|
|
9091
|
-
return { tables, columns };
|
|
9092
|
-
}
|
|
9093
|
-
isTableBlacklisted(tableName) {
|
|
9094
|
-
return this.state.tables.has(tableName.toLowerCase());
|
|
9095
|
-
}
|
|
9096
|
-
isColumnBlacklisted(tableName, columnName) {
|
|
9097
|
-
const columnSet = this.state.columns.get(tableName.toLowerCase());
|
|
9098
|
-
if (!columnSet) {
|
|
9099
|
-
return false;
|
|
9100
|
-
}
|
|
9101
|
-
return columnSet.has(columnName);
|
|
9102
|
-
}
|
|
9103
|
-
getBlacklistedColumns(tableName) {
|
|
9104
|
-
const columnSet = this.state.columns.get(tableName.toLowerCase());
|
|
9105
|
-
if (!columnSet) {
|
|
9106
|
-
return [];
|
|
9107
|
-
}
|
|
9108
|
-
return Array.from(columnSet);
|
|
9109
|
-
}
|
|
9110
|
-
getAllBlacklistedColumns() {
|
|
9111
|
-
const all = new Set;
|
|
9112
|
-
for (const columnSet of this.state.columns.values()) {
|
|
9113
|
-
for (const column of columnSet)
|
|
9114
|
-
all.add(column);
|
|
9115
|
-
}
|
|
9116
|
-
return Array.from(all);
|
|
9117
|
-
}
|
|
9118
|
-
canOverrideBlacklist() {
|
|
9119
|
-
return this.overrideEnabled;
|
|
9120
|
-
}
|
|
9121
|
-
getState() {
|
|
9122
|
-
return this.state;
|
|
9123
|
-
}
|
|
9124
|
-
}
|
|
9125
9593
|
// src/utils/es-index-target.ts
|
|
9126
9594
|
var MAX_DECODE_PASSES = 4;
|
|
9127
9595
|
function unwrap(target) {
|
|
@@ -9163,16 +9631,16 @@ function expandIndexTargets(target) {
|
|
|
9163
9631
|
return { concrete, wildcards };
|
|
9164
9632
|
}
|
|
9165
9633
|
function matchesIndexGlob(pattern, name) {
|
|
9166
|
-
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern
|
|
9634
|
+
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern;
|
|
9167
9635
|
try {
|
|
9168
|
-
return
|
|
9636
|
+
return globMatches(normalized, name, { caseInsensitive: true });
|
|
9169
9637
|
} catch {
|
|
9170
9638
|
return true;
|
|
9171
9639
|
}
|
|
9172
9640
|
}
|
|
9173
9641
|
function reachesByConvention(name, entry) {
|
|
9174
|
-
const lower = name
|
|
9175
|
-
const target = entry
|
|
9642
|
+
const lower = foldFieldPath(name);
|
|
9643
|
+
const target = foldFieldPath(entry);
|
|
9176
9644
|
return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
|
|
9177
9645
|
}
|
|
9178
9646
|
function escapeRegExp(text) {
|
|
@@ -9193,11 +9661,13 @@ function indexExpressionReaches(expression, blacklisted) {
|
|
|
9193
9661
|
return false;
|
|
9194
9662
|
const { concrete, wildcards } = expandIndexTargets(expression);
|
|
9195
9663
|
const entries = expandBlacklistEntries(blacklisted);
|
|
9196
|
-
const reachesName = (name) => entries.names.some((entry) => entry
|
|
9664
|
+
const reachesName = (name) => entries.names.some((entry) => foldFieldPath(entry) === foldFieldPath(name) || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
|
|
9197
9665
|
return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
|
|
9198
9666
|
}
|
|
9199
9667
|
|
|
9200
9668
|
// src/core/blacklist-validator.ts
|
|
9669
|
+
var FOLD_CASE2 = { caseInsensitive: true };
|
|
9670
|
+
var PRE_FOLDED = { caseInsensitive: true, alreadyFolded: true };
|
|
9201
9671
|
function flattenArrayRow(row) {
|
|
9202
9672
|
const records = [];
|
|
9203
9673
|
for (const item of row) {
|
|
@@ -9212,7 +9682,7 @@ function dedupe2(values) {
|
|
|
9212
9682
|
const seen = new Set;
|
|
9213
9683
|
const result = [];
|
|
9214
9684
|
for (const value of values) {
|
|
9215
|
-
const key = value
|
|
9685
|
+
const key = foldFieldPath(value);
|
|
9216
9686
|
if (value.length === 0 || seen.has(key))
|
|
9217
9687
|
continue;
|
|
9218
9688
|
seen.add(key);
|
|
@@ -9220,6 +9690,20 @@ function dedupe2(values) {
|
|
|
9220
9690
|
}
|
|
9221
9691
|
return result;
|
|
9222
9692
|
}
|
|
9693
|
+
function omittedByName(omittedColumns, path) {
|
|
9694
|
+
return omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`));
|
|
9695
|
+
}
|
|
9696
|
+
function compileGlobRules(rules, table, operation) {
|
|
9697
|
+
const globbed = rules.filter(isGlobPattern);
|
|
9698
|
+
if (globbed.length === 0)
|
|
9699
|
+
return [];
|
|
9700
|
+
const { patterns, rejected } = compilePatterns(globbed);
|
|
9701
|
+
if (rejected.length > 0) {
|
|
9702
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
9703
|
+
throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
|
|
9704
|
+
}
|
|
9705
|
+
return patterns;
|
|
9706
|
+
}
|
|
9223
9707
|
|
|
9224
9708
|
class BlacklistValidator {
|
|
9225
9709
|
manager;
|
|
@@ -9277,7 +9761,27 @@ class BlacklistValidator {
|
|
|
9277
9761
|
if (blacklisted.length === 0 || fields.length === 0) {
|
|
9278
9762
|
return;
|
|
9279
9763
|
}
|
|
9280
|
-
const
|
|
9764
|
+
const protectedPaths = new Set(blacklisted.map(foldFieldPath));
|
|
9765
|
+
const globs = compileGlobRules(blacklisted, tableName, operation);
|
|
9766
|
+
const conflicts = fields.filter((field) => {
|
|
9767
|
+
const name = foldFieldPath(field);
|
|
9768
|
+
if (protectedPaths.has(name))
|
|
9769
|
+
return true;
|
|
9770
|
+
if (matchAny(name, globs))
|
|
9771
|
+
return true;
|
|
9772
|
+
let dot = name.indexOf(".");
|
|
9773
|
+
while (dot >= 0) {
|
|
9774
|
+
if (dot > 0) {
|
|
9775
|
+
const ancestor = name.slice(0, dot);
|
|
9776
|
+
if (protectedPaths.has(ancestor))
|
|
9777
|
+
return true;
|
|
9778
|
+
if (matchAny(ancestor, globs))
|
|
9779
|
+
return true;
|
|
9780
|
+
}
|
|
9781
|
+
dot = name.indexOf(".", dot + 1);
|
|
9782
|
+
}
|
|
9783
|
+
return false;
|
|
9784
|
+
});
|
|
9281
9785
|
if (conflicts.length === 0) {
|
|
9282
9786
|
return;
|
|
9283
9787
|
}
|
|
@@ -9303,7 +9807,7 @@ class BlacklistValidator {
|
|
|
9303
9807
|
const tables = dedupe2(tableNames);
|
|
9304
9808
|
const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
|
|
9305
9809
|
if (blacklistedColumns.length === 0) {
|
|
9306
|
-
return { filteredRows: rows, omittedColumns: [] };
|
|
9810
|
+
return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
|
|
9307
9811
|
}
|
|
9308
9812
|
const probeNested = blacklistedColumns.some((path) => path.includes("."));
|
|
9309
9813
|
const presentColumns = new Set(columnList);
|
|
@@ -9315,7 +9819,7 @@ class BlacklistValidator {
|
|
|
9315
9819
|
continue;
|
|
9316
9820
|
const value = record[key];
|
|
9317
9821
|
if (value !== null && typeof value === "object")
|
|
9318
|
-
nestedHeads.add(key);
|
|
9822
|
+
nestedHeads.add(foldFieldPath(key));
|
|
9319
9823
|
}
|
|
9320
9824
|
};
|
|
9321
9825
|
for (const row of rows) {
|
|
@@ -9328,38 +9832,103 @@ class BlacklistValidator {
|
|
|
9328
9832
|
}
|
|
9329
9833
|
collect(row);
|
|
9330
9834
|
}
|
|
9331
|
-
const protectedPaths = new Set(blacklistedColumns);
|
|
9835
|
+
const protectedPaths = new Set(blacklistedColumns.map(foldFieldPath));
|
|
9836
|
+
const globRules = compileGlobRules(blacklistedColumns, tables[0] ?? "unknown", "SELECT");
|
|
9837
|
+
const presentByFolded = new Map;
|
|
9838
|
+
for (const column of presentColumns) {
|
|
9839
|
+
const folded = foldFieldPath(column);
|
|
9840
|
+
const names = presentByFolded.get(folded);
|
|
9841
|
+
if (names === undefined)
|
|
9842
|
+
presentByFolded.set(folded, [column]);
|
|
9843
|
+
else
|
|
9844
|
+
names.push(column);
|
|
9845
|
+
}
|
|
9332
9846
|
const omitted = new Set;
|
|
9333
9847
|
for (const path of blacklistedColumns) {
|
|
9334
|
-
|
|
9335
|
-
|
|
9848
|
+
const present = presentByFolded.get(foldFieldPath(path));
|
|
9849
|
+
if (present !== undefined) {
|
|
9850
|
+
for (const name of present)
|
|
9851
|
+
omitted.add(name);
|
|
9336
9852
|
continue;
|
|
9337
9853
|
}
|
|
9338
9854
|
const dot = path.indexOf(".");
|
|
9339
9855
|
if (dot < 0)
|
|
9340
9856
|
continue;
|
|
9341
|
-
if (!nestedHeads.has(path.slice(0, dot)))
|
|
9857
|
+
if (!nestedHeads.has(foldFieldPath(path.slice(0, dot))))
|
|
9342
9858
|
continue;
|
|
9343
|
-
const segments = path.split(".");
|
|
9344
|
-
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
9859
|
+
const segments = path.split(".").map(foldFieldPath);
|
|
9860
|
+
if (rows.some((row) => hasFieldPath(row, segments, PRE_FOLDED)))
|
|
9345
9861
|
omitted.add(path);
|
|
9346
9862
|
}
|
|
9863
|
+
const nestedGlobs = globRules.filter((pattern) => pattern.segments.length > 1);
|
|
9864
|
+
const matchedNested = [];
|
|
9865
|
+
if (nestedGlobs.length > 0 && nestedHeads.size > 0) {
|
|
9866
|
+
const pending = new Set(nestedGlobs);
|
|
9867
|
+
const states = compileNestedRules(nestedGlobs);
|
|
9868
|
+
for (const row of rows) {
|
|
9869
|
+
if (pending.size === 0)
|
|
9870
|
+
break;
|
|
9871
|
+
if (row === null || typeof row !== "object")
|
|
9872
|
+
continue;
|
|
9873
|
+
collectNestedMatches(row, states, pending, (pattern) => {
|
|
9874
|
+
matchedNested.push(pattern);
|
|
9875
|
+
omitted.add(pattern.raw);
|
|
9876
|
+
});
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9347
9879
|
for (const column of presentColumns) {
|
|
9348
9880
|
let dot = column.indexOf(".");
|
|
9349
9881
|
while (dot >= 0) {
|
|
9350
|
-
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
9882
|
+
if (dot > 0 && protectedPaths.has(foldFieldPath(column.slice(0, dot)))) {
|
|
9351
9883
|
omitted.add(column);
|
|
9352
9884
|
break;
|
|
9353
9885
|
}
|
|
9354
9886
|
dot = column.indexOf(".", dot + 1);
|
|
9355
9887
|
}
|
|
9356
9888
|
}
|
|
9889
|
+
if (globRules.length > 0) {
|
|
9890
|
+
for (const column of presentColumns) {
|
|
9891
|
+
const folded = foldFieldPath(column);
|
|
9892
|
+
if (matchAny(folded, globRules)) {
|
|
9893
|
+
omitted.add(column);
|
|
9894
|
+
continue;
|
|
9895
|
+
}
|
|
9896
|
+
let dot = folded.indexOf(".");
|
|
9897
|
+
while (dot >= 0) {
|
|
9898
|
+
if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
|
|
9899
|
+
omitted.add(column);
|
|
9900
|
+
break;
|
|
9901
|
+
}
|
|
9902
|
+
dot = folded.indexOf(".", dot + 1);
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
}
|
|
9357
9906
|
const omittedColumns = Array.from(omitted);
|
|
9358
9907
|
if (omittedColumns.length === 0) {
|
|
9359
|
-
return { filteredRows: rows, omittedColumns: [] };
|
|
9908
|
+
return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
|
|
9360
9909
|
}
|
|
9361
|
-
const
|
|
9362
|
-
|
|
9910
|
+
const removalPaths = matchedNested.length === 0 ? omittedColumns : omittedColumns.filter((path) => !matchedNested.some((p) => p.raw === path));
|
|
9911
|
+
const filteredRows = omitFieldPaths(rows, removalPaths, matchedNested.length > 0 ? { ...FOLD_CASE2, patterns: matchedNested } : FOLD_CASE2);
|
|
9912
|
+
return {
|
|
9913
|
+
filteredRows,
|
|
9914
|
+
omittedColumns,
|
|
9915
|
+
reachesOmitted: (path) => {
|
|
9916
|
+
if (omittedByName(removalPaths, path))
|
|
9917
|
+
return true;
|
|
9918
|
+
if (matchedNested.length === 0)
|
|
9919
|
+
return false;
|
|
9920
|
+
const folded = foldFieldPath(path);
|
|
9921
|
+
if (matchAny(folded, matchedNested))
|
|
9922
|
+
return true;
|
|
9923
|
+
let dot = folded.indexOf(".");
|
|
9924
|
+
while (dot > 0) {
|
|
9925
|
+
if (matchAny(folded.slice(0, dot), matchedNested))
|
|
9926
|
+
return true;
|
|
9927
|
+
dot = folded.indexOf(".", dot + 1);
|
|
9928
|
+
}
|
|
9929
|
+
return false;
|
|
9930
|
+
}
|
|
9931
|
+
};
|
|
9363
9932
|
}
|
|
9364
9933
|
buildSecurityNotification(_tableName, omittedColumns) {
|
|
9365
9934
|
if (omittedColumns.length === 0) {
|