@carllee1983/dbcli 6.0.0 → 7.0.1
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/rules/dbcli.mdc +1 -1
- package/.cursor/skills/dbcli/reference.md +26 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +1 -1
- package/.github/skills/dbcli/reference.md +26 -0
- package/CHANGELOG.md +241 -0
- package/assets/SKILL.md +1 -1
- package/assets/SKILL.zh-TW.md +1 -1
- package/assets/reference.md +26 -0
- package/dist/cli-runtime.mjs +732 -341
- package/dist/cli.mjs +4 -2
- package/dist/core.d.ts +21 -3
- package/dist/core.mjs +514 -220
- package/gemini-extension.json +1 -1
- package/package.json +4 -2
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +26 -0
- package/skills/dbcli/SKILL.md +1 -1
- package/skills/dbcli/reference.md +26 -0
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,12 @@ 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
|
+
|
|
2027
2037
|
// src/utils/glob.ts
|
|
2028
2038
|
function findClassEnd(glob, open) {
|
|
2029
2039
|
for (let i = open + 1;i < glob.length; i++) {
|
|
@@ -2045,15 +2055,19 @@ function isValidCharacterClass(body) {
|
|
|
2045
2055
|
}
|
|
2046
2056
|
}
|
|
2047
2057
|
var parsedGlobs = new Map;
|
|
2048
|
-
|
|
2049
|
-
|
|
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);
|
|
2050
2062
|
if (memo !== undefined)
|
|
2051
2063
|
return memo;
|
|
2052
|
-
const parsed = parseGlobUncached(glob);
|
|
2053
|
-
parsedGlobs.
|
|
2064
|
+
const parsed = parseGlobUncached(glob, caseInsensitive);
|
|
2065
|
+
if (parsedGlobs.size >= PARSED_GLOB_LIMIT)
|
|
2066
|
+
parsedGlobs.clear();
|
|
2067
|
+
parsedGlobs.set(key, parsed);
|
|
2054
2068
|
return parsed;
|
|
2055
2069
|
}
|
|
2056
|
-
function parseGlobUncached(glob) {
|
|
2070
|
+
function parseGlobUncached(glob, caseInsensitive) {
|
|
2057
2071
|
const runs = [];
|
|
2058
2072
|
let current = [];
|
|
2059
2073
|
let leadingStar = false;
|
|
@@ -2068,7 +2082,9 @@ function parseGlobUncached(glob) {
|
|
|
2068
2082
|
const c = glob[i];
|
|
2069
2083
|
if (c === "\\" && i + 1 < glob.length) {
|
|
2070
2084
|
trailingStar = false;
|
|
2071
|
-
|
|
2085
|
+
const escaped = String.fromCodePoint(glob.codePointAt(++i));
|
|
2086
|
+
i += escaped.length - 1;
|
|
2087
|
+
current.push(...literalToken(escaped, caseInsensitive));
|
|
2072
2088
|
continue;
|
|
2073
2089
|
}
|
|
2074
2090
|
if (c === "*") {
|
|
@@ -2088,39 +2104,53 @@ function parseGlobUncached(glob) {
|
|
|
2088
2104
|
const end = findClassEnd(glob, i);
|
|
2089
2105
|
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
2090
2106
|
if (body !== "" && isValidCharacterClass(body)) {
|
|
2091
|
-
current.push({
|
|
2107
|
+
current.push({
|
|
2108
|
+
kind: "class",
|
|
2109
|
+
test: new RegExp(`^${body}$`, caseInsensitive ? "si" : "s")
|
|
2110
|
+
});
|
|
2092
2111
|
i = end;
|
|
2093
2112
|
continue;
|
|
2094
2113
|
}
|
|
2095
|
-
current.push(
|
|
2114
|
+
current.push(...literalToken("[", caseInsensitive));
|
|
2096
2115
|
continue;
|
|
2097
2116
|
}
|
|
2098
|
-
|
|
2117
|
+
const literal = String.fromCodePoint(glob.codePointAt(i));
|
|
2118
|
+
i += literal.length - 1;
|
|
2119
|
+
current.push(...literalToken(literal, caseInsensitive));
|
|
2099
2120
|
}
|
|
2100
2121
|
endRun();
|
|
2101
2122
|
return { runs, leadingStar, trailingStar, anchored: !sawStar };
|
|
2102
2123
|
}
|
|
2103
|
-
function
|
|
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) {
|
|
2104
2129
|
if (at + run.length > text.length)
|
|
2105
2130
|
return false;
|
|
2106
2131
|
for (let i = 0;i < run.length; i++) {
|
|
2107
2132
|
const token = run[i];
|
|
2108
|
-
const ch = text[at + i];
|
|
2109
2133
|
if (token.kind === "literal") {
|
|
2110
|
-
if (token.char !==
|
|
2134
|
+
if (token.char !== text[at + i])
|
|
2111
2135
|
return false;
|
|
2112
2136
|
} else if (token.kind === "class") {
|
|
2113
|
-
|
|
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))
|
|
2114
2142
|
return false;
|
|
2115
2143
|
}
|
|
2116
2144
|
}
|
|
2117
2145
|
return true;
|
|
2118
2146
|
}
|
|
2119
|
-
function globMatches(glob,
|
|
2120
|
-
const
|
|
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;
|
|
2121
2151
|
if (anchored) {
|
|
2122
2152
|
const run = runs[0] ?? [];
|
|
2123
|
-
return text.length === run.length && runMatchesAt(run, text, 0);
|
|
2153
|
+
return text.length === run.length && runMatchesAt(run, text, subject, 0);
|
|
2124
2154
|
}
|
|
2125
2155
|
if (runs.length === 0)
|
|
2126
2156
|
return true;
|
|
@@ -2129,7 +2159,7 @@ function globMatches(glob, text) {
|
|
|
2129
2159
|
let last = runs.length;
|
|
2130
2160
|
if (!leadingStar) {
|
|
2131
2161
|
const head = runs[0];
|
|
2132
|
-
if (!runMatchesAt(head, text, 0))
|
|
2162
|
+
if (!runMatchesAt(head, text, subject, 0))
|
|
2133
2163
|
return false;
|
|
2134
2164
|
cursor = head.length;
|
|
2135
2165
|
first = 1;
|
|
@@ -2137,7 +2167,7 @@ function globMatches(glob, text) {
|
|
|
2137
2167
|
if (!trailingStar) {
|
|
2138
2168
|
const tail = runs[last - 1];
|
|
2139
2169
|
const at = text.length - tail.length;
|
|
2140
|
-
if (at < cursor || !runMatchesAt(tail, text, at))
|
|
2170
|
+
if (at < cursor || !runMatchesAt(tail, text, subject, at))
|
|
2141
2171
|
return false;
|
|
2142
2172
|
last -= 1;
|
|
2143
2173
|
}
|
|
@@ -2146,7 +2176,7 @@ function globMatches(glob, text) {
|
|
|
2146
2176
|
const run = runs[r];
|
|
2147
2177
|
let found = -1;
|
|
2148
2178
|
for (let at = cursor;at + run.length <= limit; at++) {
|
|
2149
|
-
if (runMatchesAt(run, text, at)) {
|
|
2179
|
+
if (runMatchesAt(run, text, subject, at)) {
|
|
2150
2180
|
found = at;
|
|
2151
2181
|
break;
|
|
2152
2182
|
}
|
|
@@ -2157,6 +2187,9 @@ function globMatches(glob, text) {
|
|
|
2157
2187
|
}
|
|
2158
2188
|
return true;
|
|
2159
2189
|
}
|
|
2190
|
+
function isGlobPattern(value) {
|
|
2191
|
+
return /[*?[\\]/.test(value);
|
|
2192
|
+
}
|
|
2160
2193
|
function globNeverMatches(glob) {
|
|
2161
2194
|
for (let i = 0;i < glob.length; i++) {
|
|
2162
2195
|
const c = glob[i];
|
|
@@ -2180,6 +2213,11 @@ function globNeverMatches(glob) {
|
|
|
2180
2213
|
return null;
|
|
2181
2214
|
}
|
|
2182
2215
|
|
|
2216
|
+
// src/core/blacklist-fold.ts
|
|
2217
|
+
function foldFieldPath(path) {
|
|
2218
|
+
return foldCase(path);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2183
2221
|
// src/core/mongo/path-matcher.ts
|
|
2184
2222
|
function compilePatterns(raw) {
|
|
2185
2223
|
const patterns = [];
|
|
@@ -2217,6 +2255,10 @@ function compilePatterns(raw) {
|
|
|
2217
2255
|
}
|
|
2218
2256
|
return { patterns, rejected };
|
|
2219
2257
|
}
|
|
2258
|
+
var FOLD_CASE = { caseInsensitive: true };
|
|
2259
|
+
function matchSegment(pattern, segment) {
|
|
2260
|
+
return globMatches(pattern, segment, FOLD_CASE);
|
|
2261
|
+
}
|
|
2220
2262
|
function matchAny(path, patterns) {
|
|
2221
2263
|
if (patterns.length === 0)
|
|
2222
2264
|
return false;
|
|
@@ -2231,7 +2273,7 @@ function matchAny(path, patterns) {
|
|
|
2231
2273
|
}
|
|
2232
2274
|
let ok = true;
|
|
2233
2275
|
for (let i = 0;i < pat.segments.length; i++) {
|
|
2234
|
-
if (!globMatches(pat.segments[i], pathSegments[i])) {
|
|
2276
|
+
if (!globMatches(pat.segments[i], pathSegments[i], FOLD_CASE)) {
|
|
2235
2277
|
ok = false;
|
|
2236
2278
|
break;
|
|
2237
2279
|
}
|
|
@@ -2241,6 +2283,7 @@ function matchAny(path, patterns) {
|
|
|
2241
2283
|
}
|
|
2242
2284
|
return false;
|
|
2243
2285
|
}
|
|
2286
|
+
var compiledRuleSets = new WeakMap;
|
|
2244
2287
|
|
|
2245
2288
|
// src/core/mongo/request-fields.ts
|
|
2246
2289
|
var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
|
|
@@ -2691,6 +2734,175 @@ var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
|
2691
2734
|
|
|
2692
2735
|
// src/adapters/redis/value-masker.ts
|
|
2693
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 };
|
|
2777
|
+
}
|
|
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
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
} else if (blacklistConfig.tables !== undefined) {
|
|
2789
|
+
console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
|
|
2790
|
+
}
|
|
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");
|
|
2817
|
+
}
|
|
2818
|
+
return { tables, columns };
|
|
2819
|
+
}
|
|
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
|
+
}
|
|
2828
|
+
return false;
|
|
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
|
+
}
|
|
2905
|
+
}
|
|
2694
2906
|
// src/utils/sql-lexical.ts
|
|
2695
2907
|
var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
|
|
2696
2908
|
var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
|
|
@@ -3611,18 +3823,139 @@ function projectRows(rows, selection) {
|
|
|
3611
3823
|
const columnNames = collectColumnNames(projectedRows);
|
|
3612
3824
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
3613
3825
|
}
|
|
3614
|
-
function hasFieldPath(row, segments) {
|
|
3615
|
-
|
|
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 };
|
|
3616
3858
|
}
|
|
3617
|
-
function
|
|
3618
|
-
|
|
3619
|
-
|
|
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
|
+
}));
|
|
3867
|
+
}
|
|
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(".") }));
|
|
3620
3949
|
const maskRecord = (row) => {
|
|
3621
|
-
let projected = cloneRecord(row, topLevel);
|
|
3950
|
+
let projected = cloneRecord(row, topLevel, caseInsensitive);
|
|
3622
3951
|
for (const { head, segments } of dotted) {
|
|
3623
|
-
const value = row
|
|
3624
|
-
if (value !== null && typeof value === "object")
|
|
3625
|
-
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);
|
|
3626
3959
|
}
|
|
3627
3960
|
return projected;
|
|
3628
3961
|
};
|
|
@@ -3642,11 +3975,38 @@ function maskArrayRow(row, maskRecord) {
|
|
|
3642
3975
|
});
|
|
3643
3976
|
return out;
|
|
3644
3977
|
}
|
|
3645
|
-
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) {
|
|
3646
4006
|
if (segments.length === 0)
|
|
3647
4007
|
return { found: true, value };
|
|
3648
4008
|
if (Array.isArray(value)) {
|
|
3649
|
-
const results = value.map((item) => readPath(item, segments));
|
|
4009
|
+
const results = value.map((item) => readPath(item, segments, caseInsensitive));
|
|
3650
4010
|
return {
|
|
3651
4011
|
found: results.some((result) => result.found),
|
|
3652
4012
|
value: results.map((result) => result.found ? result.value : undefined)
|
|
@@ -3654,41 +4014,68 @@ function readPath(value, segments) {
|
|
|
3654
4014
|
}
|
|
3655
4015
|
if (!isRecord(value))
|
|
3656
4016
|
return { found: false };
|
|
3657
|
-
const
|
|
3658
|
-
if (
|
|
3659
|
-
return { found: true, value: value[
|
|
3660
|
-
}
|
|
4017
|
+
const exact = ownKey(value, segments.join("."), caseInsensitive);
|
|
4018
|
+
if (exact !== undefined)
|
|
4019
|
+
return { found: true, value: value[exact] };
|
|
3661
4020
|
const [head, ...tail] = segments;
|
|
3662
|
-
|
|
4021
|
+
const headKey = ownKey(value, head, caseInsensitive);
|
|
4022
|
+
if (headKey === undefined)
|
|
3663
4023
|
return { found: false };
|
|
3664
|
-
return readPath(value[
|
|
4024
|
+
return readPath(value[headKey], tail, caseInsensitive);
|
|
3665
4025
|
}
|
|
3666
|
-
function
|
|
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;
|
|
4041
|
+
}
|
|
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) {
|
|
3667
4051
|
if (segments.length === 0)
|
|
3668
4052
|
return value;
|
|
3669
4053
|
if (Array.isArray(value))
|
|
3670
|
-
return value.map((item) => omitPath(item, segments));
|
|
4054
|
+
return value.map((item) => omitPath(item, segments, caseInsensitive));
|
|
3671
4055
|
if (!isPlainRecord(value))
|
|
3672
4056
|
return value;
|
|
3673
|
-
const
|
|
3674
|
-
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);
|
|
3675
4061
|
const out = {};
|
|
3676
4062
|
for (const [key, child] of Object.entries(value)) {
|
|
3677
|
-
|
|
4063
|
+
const folded = fold(key);
|
|
4064
|
+
if (folded === exactPath)
|
|
3678
4065
|
continue;
|
|
3679
|
-
if (
|
|
4066
|
+
if (folded === head) {
|
|
3680
4067
|
if (tail.length > 0)
|
|
3681
|
-
defineData(out, key, omitPath(child, tail));
|
|
4068
|
+
defineData(out, key, omitPath(child, tail, caseInsensitive));
|
|
3682
4069
|
continue;
|
|
3683
4070
|
}
|
|
3684
4071
|
defineData(out, key, child);
|
|
3685
4072
|
}
|
|
3686
4073
|
return out;
|
|
3687
4074
|
}
|
|
3688
|
-
function cloneRecord(value, omittedKeys) {
|
|
4075
|
+
function cloneRecord(value, omittedKeys, caseInsensitive = false) {
|
|
3689
4076
|
const out = {};
|
|
3690
4077
|
for (const [key, child] of Object.entries(value)) {
|
|
3691
|
-
if (!omittedKeys.has(key))
|
|
4078
|
+
if (!omittedKeys.has(caseInsensitive ? foldCase(key) : key))
|
|
3692
4079
|
defineData(out, key, child);
|
|
3693
4080
|
}
|
|
3694
4081
|
return out;
|
|
@@ -3794,12 +4181,12 @@ class QueryExecutor {
|
|
|
3794
4181
|
let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
|
|
3795
4182
|
let filteredRows = rows;
|
|
3796
4183
|
let securityNotification;
|
|
3797
|
-
let
|
|
4184
|
+
let reachesOmitted = () => false;
|
|
3798
4185
|
if (this.blacklistValidator) {
|
|
3799
4186
|
const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
|
|
3800
4187
|
filteredRows = filterResult.filteredRows;
|
|
3801
4188
|
if (filterResult.omittedColumns.length > 0) {
|
|
3802
|
-
|
|
4189
|
+
reachesOmitted = filterResult.reachesOmitted;
|
|
3803
4190
|
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
3804
4191
|
securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
|
|
3805
4192
|
}
|
|
@@ -3807,7 +4194,7 @@ class QueryExecutor {
|
|
|
3807
4194
|
if (options?.fieldSelection) {
|
|
3808
4195
|
const fieldSelection = options.fieldSelection.mode === "include" ? {
|
|
3809
4196
|
mode: "include",
|
|
3810
|
-
paths: options.fieldSelection.paths.filter((path) => !
|
|
4197
|
+
paths: options.fieldSelection.paths.filter((path) => !reachesOmitted(path))
|
|
3811
4198
|
} : options.fieldSelection;
|
|
3812
4199
|
const projection = projectRows(filteredRows, fieldSelection);
|
|
3813
4200
|
filteredRows = projection.rows;
|
|
@@ -9203,143 +9590,6 @@ DBCLI_PASSWORD=${password}
|
|
|
9203
9590
|
}
|
|
9204
9591
|
}
|
|
9205
9592
|
};
|
|
9206
|
-
// src/core/blacklist-manager.ts
|
|
9207
|
-
function normalizeBlacklistEntry(raw) {
|
|
9208
|
-
const trimmed = raw.trim();
|
|
9209
|
-
const quote = trimmed[0];
|
|
9210
|
-
if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
|
|
9211
|
-
return trimmed.slice(1, -1).trim();
|
|
9212
|
-
}
|
|
9213
|
-
return trimmed;
|
|
9214
|
-
}
|
|
9215
|
-
function assertNotTableQualified(tableKey, column) {
|
|
9216
|
-
const dot = column.indexOf(".");
|
|
9217
|
-
if (dot <= 0)
|
|
9218
|
-
return;
|
|
9219
|
-
if (column.slice(0, dot).toLowerCase() !== tableKey.toLowerCase())
|
|
9220
|
-
return;
|
|
9221
|
-
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))}.`);
|
|
9222
|
-
}
|
|
9223
|
-
|
|
9224
|
-
class BlacklistManager {
|
|
9225
|
-
config;
|
|
9226
|
-
state;
|
|
9227
|
-
overrideEnabled;
|
|
9228
|
-
constructor(config, overrideEnvValue) {
|
|
9229
|
-
this.config = config;
|
|
9230
|
-
this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
|
|
9231
|
-
this.state = this.loadBlacklist();
|
|
9232
|
-
this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
|
|
9233
|
-
}
|
|
9234
|
-
loadBlacklist() {
|
|
9235
|
-
const tables = new Set;
|
|
9236
|
-
const columns = new Map;
|
|
9237
|
-
const blacklistConfig = this.config.blacklist;
|
|
9238
|
-
if (!blacklistConfig) {
|
|
9239
|
-
return { tables, columns };
|
|
9240
|
-
}
|
|
9241
|
-
if (Array.isArray(blacklistConfig.tables)) {
|
|
9242
|
-
for (const tableName of blacklistConfig.tables) {
|
|
9243
|
-
if (typeof tableName === "string") {
|
|
9244
|
-
tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
|
|
9245
|
-
} else {
|
|
9246
|
-
console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
|
|
9247
|
-
}
|
|
9248
|
-
}
|
|
9249
|
-
} else if (blacklistConfig.tables !== undefined) {
|
|
9250
|
-
console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
|
|
9251
|
-
}
|
|
9252
|
-
if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
|
|
9253
|
-
for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
|
|
9254
|
-
if (typeof tableName !== "string") {
|
|
9255
|
-
console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
|
|
9256
|
-
continue;
|
|
9257
|
-
}
|
|
9258
|
-
if (!Array.isArray(cols)) {
|
|
9259
|
-
console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
|
|
9260
|
-
continue;
|
|
9261
|
-
}
|
|
9262
|
-
const columnSet = new Set;
|
|
9263
|
-
for (const col of cols) {
|
|
9264
|
-
if (typeof col === "string") {
|
|
9265
|
-
const entry = normalizeBlacklistEntry(col);
|
|
9266
|
-
assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
|
|
9267
|
-
columnSet.add(entry);
|
|
9268
|
-
} else {
|
|
9269
|
-
console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
|
|
9270
|
-
}
|
|
9271
|
-
}
|
|
9272
|
-
if (columnSet.size > 0) {
|
|
9273
|
-
columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
|
|
9274
|
-
}
|
|
9275
|
-
}
|
|
9276
|
-
} else if (blacklistConfig.columns !== undefined) {
|
|
9277
|
-
console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
|
|
9278
|
-
}
|
|
9279
|
-
return { tables, columns };
|
|
9280
|
-
}
|
|
9281
|
-
isTableBlacklisted(tableName) {
|
|
9282
|
-
const name = tableName.toLowerCase();
|
|
9283
|
-
if (this.state.tables.has(name))
|
|
9284
|
-
return true;
|
|
9285
|
-
for (const pattern of this.wildcardTables) {
|
|
9286
|
-
if (globMatches(pattern, name))
|
|
9287
|
-
return true;
|
|
9288
|
-
}
|
|
9289
|
-
return false;
|
|
9290
|
-
}
|
|
9291
|
-
wildcardTables;
|
|
9292
|
-
columnRulesFor(tableName) {
|
|
9293
|
-
const key = normalizeBlacklistEntry(tableName).toLowerCase();
|
|
9294
|
-
const direct = this.state.columns.get(key);
|
|
9295
|
-
if (direct)
|
|
9296
|
-
return direct;
|
|
9297
|
-
const dot = key.lastIndexOf(".");
|
|
9298
|
-
if (dot > 0) {
|
|
9299
|
-
const bare = this.state.columns.get(key.slice(dot + 1));
|
|
9300
|
-
if (bare)
|
|
9301
|
-
return bare;
|
|
9302
|
-
}
|
|
9303
|
-
let merged;
|
|
9304
|
-
for (const [ruleKey, columns] of this.state.columns) {
|
|
9305
|
-
const ruleDot = ruleKey.lastIndexOf(".");
|
|
9306
|
-
if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
|
|
9307
|
-
continue;
|
|
9308
|
-
merged ??= new Set;
|
|
9309
|
-
for (const column of columns)
|
|
9310
|
-
merged.add(column);
|
|
9311
|
-
}
|
|
9312
|
-
return merged;
|
|
9313
|
-
}
|
|
9314
|
-
isColumnBlacklisted(tableName, columnName) {
|
|
9315
|
-
const columnSet = this.columnRulesFor(tableName);
|
|
9316
|
-
if (!columnSet) {
|
|
9317
|
-
return false;
|
|
9318
|
-
}
|
|
9319
|
-
return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
|
|
9320
|
-
}
|
|
9321
|
-
getBlacklistedColumns(tableName) {
|
|
9322
|
-
const columnSet = this.columnRulesFor(tableName);
|
|
9323
|
-
if (!columnSet) {
|
|
9324
|
-
return [];
|
|
9325
|
-
}
|
|
9326
|
-
return Array.from(columnSet);
|
|
9327
|
-
}
|
|
9328
|
-
getAllBlacklistedColumns() {
|
|
9329
|
-
const all = new Set;
|
|
9330
|
-
for (const columnSet of this.state.columns.values()) {
|
|
9331
|
-
for (const column of columnSet)
|
|
9332
|
-
all.add(column);
|
|
9333
|
-
}
|
|
9334
|
-
return Array.from(all);
|
|
9335
|
-
}
|
|
9336
|
-
canOverrideBlacklist() {
|
|
9337
|
-
return this.overrideEnabled;
|
|
9338
|
-
}
|
|
9339
|
-
getState() {
|
|
9340
|
-
return this.state;
|
|
9341
|
-
}
|
|
9342
|
-
}
|
|
9343
9593
|
// src/utils/es-index-target.ts
|
|
9344
9594
|
var MAX_DECODE_PASSES = 4;
|
|
9345
9595
|
function unwrap(target) {
|
|
@@ -9381,16 +9631,16 @@ function expandIndexTargets(target) {
|
|
|
9381
9631
|
return { concrete, wildcards };
|
|
9382
9632
|
}
|
|
9383
9633
|
function matchesIndexGlob(pattern, name) {
|
|
9384
|
-
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern
|
|
9634
|
+
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern;
|
|
9385
9635
|
try {
|
|
9386
|
-
return globMatches(normalized, name
|
|
9636
|
+
return globMatches(normalized, name, { caseInsensitive: true });
|
|
9387
9637
|
} catch {
|
|
9388
9638
|
return true;
|
|
9389
9639
|
}
|
|
9390
9640
|
}
|
|
9391
9641
|
function reachesByConvention(name, entry) {
|
|
9392
|
-
const lower = name
|
|
9393
|
-
const target = entry
|
|
9642
|
+
const lower = foldFieldPath(name);
|
|
9643
|
+
const target = foldFieldPath(entry);
|
|
9394
9644
|
return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
|
|
9395
9645
|
}
|
|
9396
9646
|
function escapeRegExp(text) {
|
|
@@ -9411,11 +9661,13 @@ function indexExpressionReaches(expression, blacklisted) {
|
|
|
9411
9661
|
return false;
|
|
9412
9662
|
const { concrete, wildcards } = expandIndexTargets(expression);
|
|
9413
9663
|
const entries = expandBlacklistEntries(blacklisted);
|
|
9414
|
-
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));
|
|
9415
9665
|
return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
|
|
9416
9666
|
}
|
|
9417
9667
|
|
|
9418
9668
|
// src/core/blacklist-validator.ts
|
|
9669
|
+
var FOLD_CASE2 = { caseInsensitive: true };
|
|
9670
|
+
var PRE_FOLDED = { caseInsensitive: true, alreadyFolded: true };
|
|
9419
9671
|
function flattenArrayRow(row) {
|
|
9420
9672
|
const records = [];
|
|
9421
9673
|
for (const item of row) {
|
|
@@ -9430,7 +9682,7 @@ function dedupe2(values) {
|
|
|
9430
9682
|
const seen = new Set;
|
|
9431
9683
|
const result = [];
|
|
9432
9684
|
for (const value of values) {
|
|
9433
|
-
const key = value
|
|
9685
|
+
const key = foldFieldPath(value);
|
|
9434
9686
|
if (value.length === 0 || seen.has(key))
|
|
9435
9687
|
continue;
|
|
9436
9688
|
seen.add(key);
|
|
@@ -9438,11 +9690,14 @@ function dedupe2(values) {
|
|
|
9438
9690
|
}
|
|
9439
9691
|
return result;
|
|
9440
9692
|
}
|
|
9441
|
-
function
|
|
9442
|
-
|
|
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);
|
|
9443
9698
|
if (globbed.length === 0)
|
|
9444
9699
|
return [];
|
|
9445
|
-
const { patterns, rejected } = compilePatterns(globbed
|
|
9700
|
+
const { patterns, rejected } = compilePatterns(globbed);
|
|
9446
9701
|
if (rejected.length > 0) {
|
|
9447
9702
|
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
9448
9703
|
throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
|
|
@@ -9506,10 +9761,10 @@ class BlacklistValidator {
|
|
|
9506
9761
|
if (blacklisted.length === 0 || fields.length === 0) {
|
|
9507
9762
|
return;
|
|
9508
9763
|
}
|
|
9509
|
-
const protectedPaths = new Set(blacklisted.map(
|
|
9510
|
-
const globs = compileGlobRules(blacklisted,
|
|
9764
|
+
const protectedPaths = new Set(blacklisted.map(foldFieldPath));
|
|
9765
|
+
const globs = compileGlobRules(blacklisted, tableName, operation);
|
|
9511
9766
|
const conflicts = fields.filter((field) => {
|
|
9512
|
-
const name = field
|
|
9767
|
+
const name = foldFieldPath(field);
|
|
9513
9768
|
if (protectedPaths.has(name))
|
|
9514
9769
|
return true;
|
|
9515
9770
|
if (matchAny(name, globs))
|
|
@@ -9552,7 +9807,7 @@ class BlacklistValidator {
|
|
|
9552
9807
|
const tables = dedupe2(tableNames);
|
|
9553
9808
|
const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
|
|
9554
9809
|
if (blacklistedColumns.length === 0) {
|
|
9555
|
-
return { filteredRows: rows, omittedColumns: [] };
|
|
9810
|
+
return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
|
|
9556
9811
|
}
|
|
9557
9812
|
const probeNested = blacklistedColumns.some((path) => path.includes("."));
|
|
9558
9813
|
const presentColumns = new Set(columnList);
|
|
@@ -9564,7 +9819,7 @@ class BlacklistValidator {
|
|
|
9564
9819
|
continue;
|
|
9565
9820
|
const value = record[key];
|
|
9566
9821
|
if (value !== null && typeof value === "object")
|
|
9567
|
-
nestedHeads.add(key);
|
|
9822
|
+
nestedHeads.add(foldFieldPath(key));
|
|
9568
9823
|
}
|
|
9569
9824
|
};
|
|
9570
9825
|
for (const row of rows) {
|
|
@@ -9577,35 +9832,54 @@ class BlacklistValidator {
|
|
|
9577
9832
|
}
|
|
9578
9833
|
collect(row);
|
|
9579
9834
|
}
|
|
9580
|
-
const
|
|
9581
|
-
|
|
9582
|
-
return dot < 0 ? path.toLowerCase() : path.slice(0, dot).toLowerCase() + path.slice(dot);
|
|
9583
|
-
};
|
|
9584
|
-
const protectedPaths = new Set(blacklistedColumns.map(foldHead));
|
|
9585
|
-
const globRules = compileGlobRules(blacklistedColumns, foldHead, tables[0] ?? "unknown", "SELECT");
|
|
9835
|
+
const protectedPaths = new Set(blacklistedColumns.map(foldFieldPath));
|
|
9836
|
+
const globRules = compileGlobRules(blacklistedColumns, tables[0] ?? "unknown", "SELECT");
|
|
9586
9837
|
const presentByFolded = new Map;
|
|
9587
|
-
for (const column of presentColumns)
|
|
9588
|
-
|
|
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
|
+
}
|
|
9589
9846
|
const omitted = new Set;
|
|
9590
9847
|
for (const path of blacklistedColumns) {
|
|
9591
|
-
const present = presentByFolded.get(
|
|
9848
|
+
const present = presentByFolded.get(foldFieldPath(path));
|
|
9592
9849
|
if (present !== undefined) {
|
|
9593
|
-
|
|
9850
|
+
for (const name of present)
|
|
9851
|
+
omitted.add(name);
|
|
9594
9852
|
continue;
|
|
9595
9853
|
}
|
|
9596
9854
|
const dot = path.indexOf(".");
|
|
9597
9855
|
if (dot < 0)
|
|
9598
9856
|
continue;
|
|
9599
|
-
if (!nestedHeads.has(path.slice(0, dot)))
|
|
9857
|
+
if (!nestedHeads.has(foldFieldPath(path.slice(0, dot))))
|
|
9600
9858
|
continue;
|
|
9601
|
-
const segments = path.split(".");
|
|
9602
|
-
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
9859
|
+
const segments = path.split(".").map(foldFieldPath);
|
|
9860
|
+
if (rows.some((row) => hasFieldPath(row, segments, PRE_FOLDED)))
|
|
9603
9861
|
omitted.add(path);
|
|
9604
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
|
+
}
|
|
9605
9879
|
for (const column of presentColumns) {
|
|
9606
9880
|
let dot = column.indexOf(".");
|
|
9607
9881
|
while (dot >= 0) {
|
|
9608
|
-
if (dot > 0 && protectedPaths.has(column.slice(0, dot)
|
|
9882
|
+
if (dot > 0 && protectedPaths.has(foldFieldPath(column.slice(0, dot)))) {
|
|
9609
9883
|
omitted.add(column);
|
|
9610
9884
|
break;
|
|
9611
9885
|
}
|
|
@@ -9614,7 +9888,7 @@ class BlacklistValidator {
|
|
|
9614
9888
|
}
|
|
9615
9889
|
if (globRules.length > 0) {
|
|
9616
9890
|
for (const column of presentColumns) {
|
|
9617
|
-
const folded =
|
|
9891
|
+
const folded = foldFieldPath(column);
|
|
9618
9892
|
if (matchAny(folded, globRules)) {
|
|
9619
9893
|
omitted.add(column);
|
|
9620
9894
|
continue;
|
|
@@ -9631,10 +9905,30 @@ class BlacklistValidator {
|
|
|
9631
9905
|
}
|
|
9632
9906
|
const omittedColumns = Array.from(omitted);
|
|
9633
9907
|
if (omittedColumns.length === 0) {
|
|
9634
|
-
return { filteredRows: rows, omittedColumns: [] };
|
|
9908
|
+
return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
|
|
9635
9909
|
}
|
|
9636
|
-
const
|
|
9637
|
-
|
|
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
|
+
};
|
|
9638
9932
|
}
|
|
9639
9933
|
buildSecurityNotification(_tableName, omittedColumns) {
|
|
9640
9934
|
if (omittedColumns.length === 0) {
|