@carllee1983/dbcli 1.47.0 → 1.48.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/rules/dbcli.mdc +4 -1
- package/.cursor/skills/dbcli/reference.md +15 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +4 -1
- package/.github/skills/dbcli/reference.md +15 -1
- package/CHANGELOG.md +95 -0
- package/assets/SKILL.md +4 -1
- package/assets/reference.md +15 -1
- package/dist/cli.mjs +18839 -7091
- package/dist/core.d.ts +86 -3
- package/dist/core.mjs +617 -58
- package/gemini-extension.json +1 -1
- package/package.json +3 -2
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +4 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +15 -1
- package/skills/dbcli/SKILL.md +4 -1
- package/skills/dbcli/reference.md +15 -1
package/dist/core.mjs
CHANGED
|
@@ -15819,22 +15819,27 @@ function truncateResult(command, reply, opts) {
|
|
|
15819
15819
|
return { value: reply };
|
|
15820
15820
|
}
|
|
15821
15821
|
|
|
15822
|
-
// src/
|
|
15822
|
+
// src/utils/glob.ts
|
|
15823
15823
|
function globToRegex(glob) {
|
|
15824
15824
|
let out = "^";
|
|
15825
15825
|
for (let i = 0;i < glob.length; i++) {
|
|
15826
15826
|
const c = glob[i];
|
|
15827
|
+
if (c === "\\" && i + 1 < glob.length) {
|
|
15828
|
+
out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15829
|
+
continue;
|
|
15830
|
+
}
|
|
15827
15831
|
if (c === "*")
|
|
15828
15832
|
out += ".*";
|
|
15829
15833
|
else if (c === "?")
|
|
15830
15834
|
out += ".";
|
|
15831
15835
|
else if (c === "[") {
|
|
15832
|
-
const end = glob
|
|
15833
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
out += glob.slice(i, end + 1);
|
|
15836
|
+
const end = findClassEnd(glob, i);
|
|
15837
|
+
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
15838
|
+
if (body !== "" && isValidCharacterClass(body)) {
|
|
15839
|
+
out += body;
|
|
15837
15840
|
i = end;
|
|
15841
|
+
} else {
|
|
15842
|
+
out += "\\[";
|
|
15838
15843
|
}
|
|
15839
15844
|
} else if (".^$+(){}|\\".includes(c)) {
|
|
15840
15845
|
out += "\\" + c;
|
|
@@ -15845,6 +15850,27 @@ function globToRegex(glob) {
|
|
|
15845
15850
|
out += "$";
|
|
15846
15851
|
return new RegExp(out);
|
|
15847
15852
|
}
|
|
15853
|
+
function findClassEnd(glob, open) {
|
|
15854
|
+
for (let i = open + 1;i < glob.length; i++) {
|
|
15855
|
+
if (glob[i] === "\\") {
|
|
15856
|
+
i++;
|
|
15857
|
+
continue;
|
|
15858
|
+
}
|
|
15859
|
+
if (glob[i] === "]")
|
|
15860
|
+
return i;
|
|
15861
|
+
}
|
|
15862
|
+
return -1;
|
|
15863
|
+
}
|
|
15864
|
+
function isValidCharacterClass(body) {
|
|
15865
|
+
try {
|
|
15866
|
+
new RegExp(body);
|
|
15867
|
+
return true;
|
|
15868
|
+
} catch {
|
|
15869
|
+
return false;
|
|
15870
|
+
}
|
|
15871
|
+
}
|
|
15872
|
+
|
|
15873
|
+
// src/adapters/redis/blacklist-enforcer.ts
|
|
15848
15874
|
function patternsOverlap(a, b) {
|
|
15849
15875
|
const ra = globToRegex(a);
|
|
15850
15876
|
const rb = globToRegex(b);
|
|
@@ -16801,6 +16827,32 @@ class AdapterFactory {
|
|
|
16801
16827
|
return AdapterFactory.createQueryableAdapter(options);
|
|
16802
16828
|
}
|
|
16803
16829
|
}
|
|
16830
|
+
// src/utils/sql-lexical.ts
|
|
16831
|
+
var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
|
|
16832
|
+
var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
|
|
16833
|
+
var DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
|
|
16834
|
+
function dollarQuoteDelimiterAt(sql, index) {
|
|
16835
|
+
if (sql[index] !== "$")
|
|
16836
|
+
return;
|
|
16837
|
+
if (continuesIdentifier(sql, index))
|
|
16838
|
+
return;
|
|
16839
|
+
return sql.slice(index).match(DOLLAR_QUOTE_DELIMITER)?.[0];
|
|
16840
|
+
}
|
|
16841
|
+
function continuesIdentifier(sql, index) {
|
|
16842
|
+
let start = index;
|
|
16843
|
+
while (start > 0 && IDENTIFIER_CONTINUATION.test(sql[start - 1] ?? ""))
|
|
16844
|
+
start--;
|
|
16845
|
+
if (start === index)
|
|
16846
|
+
return false;
|
|
16847
|
+
const run = sql.slice(start, index);
|
|
16848
|
+
const first = run[0];
|
|
16849
|
+
if (IDENTIFIER_START.test(first))
|
|
16850
|
+
return true;
|
|
16851
|
+
if (first === "$")
|
|
16852
|
+
return false;
|
|
16853
|
+
return run.replace(/^[0-9]+(?:[eE][0-9]+)?/, "").length > 0;
|
|
16854
|
+
}
|
|
16855
|
+
|
|
16804
16856
|
// src/core/permission-guard.ts
|
|
16805
16857
|
class PermissionError extends Error {
|
|
16806
16858
|
classification;
|
|
@@ -16859,11 +16911,19 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16859
16911
|
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
16860
16912
|
continue;
|
|
16861
16913
|
}
|
|
16914
|
+
const nests = options.dialect === "postgresql";
|
|
16915
|
+
let depth = 1;
|
|
16862
16916
|
i += 2;
|
|
16863
|
-
while (i < sql.length) {
|
|
16917
|
+
while (i < sql.length && depth > 0) {
|
|
16918
|
+
if (nests && sql[i] === "/" && sql[i + 1] === "*") {
|
|
16919
|
+
depth++;
|
|
16920
|
+
i += 2;
|
|
16921
|
+
continue;
|
|
16922
|
+
}
|
|
16864
16923
|
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
16924
|
+
depth--;
|
|
16865
16925
|
i += 2;
|
|
16866
|
-
|
|
16926
|
+
continue;
|
|
16867
16927
|
}
|
|
16868
16928
|
i++;
|
|
16869
16929
|
}
|
|
@@ -16871,7 +16931,7 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16871
16931
|
continue;
|
|
16872
16932
|
}
|
|
16873
16933
|
if (options.dialect === "postgresql" && char === "$") {
|
|
16874
|
-
const delimiter = sql
|
|
16934
|
+
const delimiter = dollarQuoteDelimiterAt(sql, i);
|
|
16875
16935
|
if (delimiter) {
|
|
16876
16936
|
i += delimiter.length;
|
|
16877
16937
|
const closingIndex = sql.indexOf(delimiter, i);
|
|
@@ -16899,7 +16959,7 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16899
16959
|
if (char === "'" || char === '"') {
|
|
16900
16960
|
const quote = char;
|
|
16901
16961
|
const quoteIndex = i;
|
|
16902
|
-
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") &&
|
|
16962
|
+
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") && !IDENTIFIER_CONTINUATION.test(sql[quoteIndex - 2] ?? "");
|
|
16903
16963
|
const backslashEscapes = options.dialect === undefined || postgresEscapeString;
|
|
16904
16964
|
i++;
|
|
16905
16965
|
while (i < sql.length) {
|
|
@@ -17075,8 +17135,59 @@ function classifyStatement(sql) {
|
|
|
17075
17135
|
confidence: determineConfidence(type, firstKeyword, upper)
|
|
17076
17136
|
};
|
|
17077
17137
|
}
|
|
17078
|
-
|
|
17079
|
-
|
|
17138
|
+
var SQL_DIALECTS = ["postgresql", "mysql", "mariadb"];
|
|
17139
|
+
var SQL_WRITE_OR_DDL_KEYWORDS = /(?<![.\w])(INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO)\b(?!\s*\()/i;
|
|
17140
|
+
var SQL_LOCK_CLAUSE = /\bFOR\s+(?:NO\s+KEY\s+)?UPDATE\b|\bFOR\s+(?:KEY\s+)?SHARE\b/gi;
|
|
17141
|
+
function findWriteKeyword(sql, dialects) {
|
|
17142
|
+
const candidates = dialects && dialects.length > 0 ? dialects : SQL_DIALECTS;
|
|
17143
|
+
for (const dialect of candidates) {
|
|
17144
|
+
const executable = stripCommentsAndStrings(sql, { dialect }).replace(SQL_LOCK_CLAUSE, " ");
|
|
17145
|
+
const match = executable.match(SQL_WRITE_OR_DDL_KEYWORDS);
|
|
17146
|
+
if (match?.[1])
|
|
17147
|
+
return match[1].toUpperCase();
|
|
17148
|
+
}
|
|
17149
|
+
return;
|
|
17150
|
+
}
|
|
17151
|
+
function containsMultipleStatements(sql, dialect) {
|
|
17152
|
+
const statementCount = (candidate) => stripCommentsAndStrings(sql, { dialect: candidate }).split(";").filter((part) => part.trim().length > 0).length;
|
|
17153
|
+
if (dialect)
|
|
17154
|
+
return statementCount(dialect) > 1;
|
|
17155
|
+
return SQL_DIALECTS.some((candidate) => statementCount(candidate) > 1);
|
|
17156
|
+
}
|
|
17157
|
+
var ESCALATABLE_READ_TYPES = new Set(["SELECT", "EXPLAIN", "DESCRIBE"]);
|
|
17158
|
+
function escalateHiddenWrite(sql, classification, dialect) {
|
|
17159
|
+
if (!ESCALATABLE_READ_TYPES.has(classification.type))
|
|
17160
|
+
return classification;
|
|
17161
|
+
const plansOnly = classification.type === "EXPLAIN" || classification.type === "DESCRIBE";
|
|
17162
|
+
if (plansOnly && !/\bANALYZE\b/i.test(sql))
|
|
17163
|
+
return classification;
|
|
17164
|
+
const hidden = findWriteKeyword(sql, dialect ? [dialect] : undefined);
|
|
17165
|
+
if (!hidden)
|
|
17166
|
+
return classification;
|
|
17167
|
+
return {
|
|
17168
|
+
...classification,
|
|
17169
|
+
type: "UNKNOWN",
|
|
17170
|
+
isDangerous: true,
|
|
17171
|
+
confidence: "HIGH",
|
|
17172
|
+
escalatedFrom: hidden
|
|
17173
|
+
};
|
|
17174
|
+
}
|
|
17175
|
+
function checkPermission(sql, permission, dialect) {
|
|
17176
|
+
const classification = escalateHiddenWrite(sql, classifyStatement(sql), dialect);
|
|
17177
|
+
if (permission !== "admin" && containsMultipleStatements(sql, dialect)) {
|
|
17178
|
+
return {
|
|
17179
|
+
allowed: false,
|
|
17180
|
+
reason: "SQL containing multiple statements is refused below admin permission, because only " + "the first statement determines the permission check. Run each statement separately.",
|
|
17181
|
+
classification
|
|
17182
|
+
};
|
|
17183
|
+
}
|
|
17184
|
+
if (permission !== "admin" && classification.escalatedFrom) {
|
|
17185
|
+
return {
|
|
17186
|
+
allowed: false,
|
|
17187
|
+
reason: `This statement opens as a read but contains an executable ` + `${classification.escalatedFrom}. A write hidden inside a read statement ` + `requires admin permission (current level: ${permission}).`,
|
|
17188
|
+
classification
|
|
17189
|
+
};
|
|
17190
|
+
}
|
|
17080
17191
|
if (permission === "admin") {
|
|
17081
17192
|
return {
|
|
17082
17193
|
allowed: true,
|
|
@@ -17136,8 +17247,8 @@ function checkPermission(sql, permission) {
|
|
|
17136
17247
|
classification
|
|
17137
17248
|
};
|
|
17138
17249
|
}
|
|
17139
|
-
function enforcePermission(sql, permission) {
|
|
17140
|
-
const result = checkPermission(sql, permission);
|
|
17250
|
+
function enforcePermission(sql, permission, dialect) {
|
|
17251
|
+
const result = checkPermission(sql, permission, dialect);
|
|
17141
17252
|
if (!result.allowed) {
|
|
17142
17253
|
throw new PermissionError(result.reason, result.classification, permission);
|
|
17143
17254
|
}
|
|
@@ -17250,28 +17361,345 @@ async function getAllTablesFromAdapter(adapter) {
|
|
|
17250
17361
|
}
|
|
17251
17362
|
}
|
|
17252
17363
|
|
|
17253
|
-
// src/utils/
|
|
17254
|
-
var
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17364
|
+
// src/utils/sql-tables.ts
|
|
17365
|
+
var TABLE_INTRODUCERS = new Set([
|
|
17366
|
+
"FROM",
|
|
17367
|
+
"JOIN",
|
|
17368
|
+
"INTO",
|
|
17369
|
+
"UPDATE",
|
|
17370
|
+
"TABLE",
|
|
17371
|
+
"TRUNCATE",
|
|
17372
|
+
"COPY",
|
|
17373
|
+
"USING",
|
|
17374
|
+
"STRAIGHT_JOIN"
|
|
17375
|
+
]);
|
|
17376
|
+
var PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
|
|
17377
|
+
var SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
|
|
17378
|
+
var AS_KEYWORD = new Set(["AS"]);
|
|
17379
|
+
var POST_TABLE_KEYWORDS = new Set([
|
|
17380
|
+
"AS",
|
|
17381
|
+
"ON",
|
|
17382
|
+
"USING",
|
|
17383
|
+
"WHERE",
|
|
17384
|
+
"GROUP",
|
|
17385
|
+
"ORDER",
|
|
17386
|
+
"HAVING",
|
|
17387
|
+
"LIMIT",
|
|
17388
|
+
"OFFSET",
|
|
17389
|
+
"FETCH",
|
|
17390
|
+
"WINDOW",
|
|
17391
|
+
"UNION",
|
|
17392
|
+
"INTERSECT",
|
|
17393
|
+
"EXCEPT",
|
|
17394
|
+
"JOIN",
|
|
17395
|
+
"INNER",
|
|
17396
|
+
"LEFT",
|
|
17397
|
+
"RIGHT",
|
|
17398
|
+
"FULL",
|
|
17399
|
+
"OUTER",
|
|
17400
|
+
"CROSS",
|
|
17401
|
+
"NATURAL",
|
|
17402
|
+
"STRAIGHT_JOIN",
|
|
17403
|
+
"SET",
|
|
17404
|
+
"VALUES",
|
|
17405
|
+
"SELECT",
|
|
17406
|
+
"RETURNING",
|
|
17407
|
+
"FOR",
|
|
17408
|
+
"INTO",
|
|
17409
|
+
"PARTITION",
|
|
17410
|
+
"WITH",
|
|
17411
|
+
"TABLESAMPLE",
|
|
17412
|
+
"FORCE",
|
|
17413
|
+
"IGNORE",
|
|
17414
|
+
"USE"
|
|
17415
|
+
]);
|
|
17416
|
+
var RESERVED_KEYWORDS = new Set([
|
|
17417
|
+
"ALL",
|
|
17418
|
+
"AND",
|
|
17419
|
+
"AS",
|
|
17420
|
+
"ASC",
|
|
17421
|
+
"CASE",
|
|
17422
|
+
"CROSS",
|
|
17423
|
+
"DESC",
|
|
17424
|
+
"DISTINCT",
|
|
17425
|
+
"ELSE",
|
|
17426
|
+
"FALSE",
|
|
17427
|
+
"FOR",
|
|
17428
|
+
"FROM",
|
|
17429
|
+
"GROUP",
|
|
17430
|
+
"HAVING",
|
|
17431
|
+
"IN",
|
|
17432
|
+
"INNER",
|
|
17433
|
+
"INTO",
|
|
17434
|
+
"IS",
|
|
17435
|
+
"JOIN",
|
|
17436
|
+
"LEFT",
|
|
17437
|
+
"LIKE",
|
|
17438
|
+
"LIMIT",
|
|
17439
|
+
"NOT",
|
|
17440
|
+
"NULL",
|
|
17441
|
+
"ON",
|
|
17442
|
+
"OR",
|
|
17443
|
+
"ORDER",
|
|
17444
|
+
"OUTER",
|
|
17445
|
+
"RIGHT",
|
|
17446
|
+
"SELECT",
|
|
17447
|
+
"THEN",
|
|
17448
|
+
"TRUE",
|
|
17449
|
+
"UNION",
|
|
17450
|
+
"USING",
|
|
17451
|
+
"VALUES",
|
|
17452
|
+
"WHEN",
|
|
17453
|
+
"WHERE",
|
|
17454
|
+
"WITH"
|
|
17455
|
+
]);
|
|
17456
|
+
var IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
|
|
17457
|
+
var IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
|
|
17458
|
+
function tokenize(sql, dialect, backslashEscapes) {
|
|
17459
|
+
const tokens = [];
|
|
17460
|
+
const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
|
|
17461
|
+
let i = 0;
|
|
17462
|
+
while (i < sql.length) {
|
|
17463
|
+
const char = sql[i];
|
|
17464
|
+
const dashFollowerCode = sql.charCodeAt(i + 2);
|
|
17465
|
+
if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
|
|
17466
|
+
while (i < sql.length && sql[i] !== `
|
|
17467
|
+
`)
|
|
17468
|
+
i++;
|
|
17469
|
+
continue;
|
|
17470
|
+
}
|
|
17471
|
+
if (mysqlDialect && char === "#") {
|
|
17472
|
+
while (i < sql.length && sql[i] !== `
|
|
17473
|
+
`)
|
|
17474
|
+
i++;
|
|
17475
|
+
continue;
|
|
17476
|
+
}
|
|
17477
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
17478
|
+
if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
|
|
17479
|
+
const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
|
|
17480
|
+
const closingIndex = sql.indexOf("*/", i + prefixLength);
|
|
17481
|
+
const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
|
|
17482
|
+
const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
|
|
17483
|
+
tokens.push(...tokenize(body, dialect, backslashEscapes));
|
|
17484
|
+
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
17485
|
+
continue;
|
|
17486
|
+
}
|
|
17487
|
+
const nests = dialect === "postgresql";
|
|
17488
|
+
let depth = 1;
|
|
17489
|
+
i += 2;
|
|
17490
|
+
while (i < sql.length && depth > 0) {
|
|
17491
|
+
if (nests && sql[i] === "/" && sql[i + 1] === "*") {
|
|
17492
|
+
depth++;
|
|
17493
|
+
i += 2;
|
|
17494
|
+
continue;
|
|
17495
|
+
}
|
|
17496
|
+
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
17497
|
+
depth--;
|
|
17498
|
+
i += 2;
|
|
17499
|
+
continue;
|
|
17500
|
+
}
|
|
17501
|
+
i++;
|
|
17502
|
+
}
|
|
17503
|
+
continue;
|
|
17504
|
+
}
|
|
17505
|
+
if (dialect === "postgresql" && char === "$") {
|
|
17506
|
+
const delimiter = dollarQuoteDelimiterAt(sql, i);
|
|
17507
|
+
if (delimiter) {
|
|
17508
|
+
i += delimiter.length;
|
|
17509
|
+
const closingIndex = sql.indexOf(delimiter, i);
|
|
17510
|
+
i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
|
|
17511
|
+
continue;
|
|
17512
|
+
}
|
|
17513
|
+
}
|
|
17514
|
+
if (char === "'") {
|
|
17515
|
+
i++;
|
|
17516
|
+
while (i < sql.length) {
|
|
17517
|
+
if (sql[i] === "'") {
|
|
17518
|
+
if (sql[i + 1] === "'") {
|
|
17519
|
+
i += 2;
|
|
17520
|
+
continue;
|
|
17521
|
+
}
|
|
17522
|
+
i++;
|
|
17523
|
+
break;
|
|
17524
|
+
}
|
|
17525
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
17526
|
+
i += 2;
|
|
17527
|
+
continue;
|
|
17528
|
+
}
|
|
17529
|
+
i++;
|
|
17530
|
+
}
|
|
17531
|
+
continue;
|
|
17532
|
+
}
|
|
17533
|
+
if (char === '"' || char === "`") {
|
|
17534
|
+
const quote = char;
|
|
17535
|
+
i++;
|
|
17536
|
+
let value = "";
|
|
17537
|
+
while (i < sql.length) {
|
|
17538
|
+
if (sql[i] === quote) {
|
|
17539
|
+
if (sql[i + 1] === quote) {
|
|
17540
|
+
value += quote;
|
|
17541
|
+
i += 2;
|
|
17542
|
+
continue;
|
|
17543
|
+
}
|
|
17544
|
+
i++;
|
|
17545
|
+
break;
|
|
17546
|
+
}
|
|
17547
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
17548
|
+
value += sql[i + 1] ?? "";
|
|
17549
|
+
i += 2;
|
|
17550
|
+
continue;
|
|
17551
|
+
}
|
|
17552
|
+
value += sql[i];
|
|
17553
|
+
i++;
|
|
17554
|
+
}
|
|
17555
|
+
tokens.push({ value, kind: "identifier", quoted: true });
|
|
17556
|
+
continue;
|
|
17557
|
+
}
|
|
17558
|
+
if (IDENTIFIER_START2.test(char)) {
|
|
17559
|
+
let value = "";
|
|
17560
|
+
while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
|
|
17561
|
+
value += sql[i];
|
|
17562
|
+
i++;
|
|
17563
|
+
}
|
|
17564
|
+
tokens.push({ value, kind: "identifier", quoted: false });
|
|
17565
|
+
continue;
|
|
17566
|
+
}
|
|
17567
|
+
if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
|
|
17568
|
+
tokens.push({ value: char, kind: "punctuation", quoted: false });
|
|
17569
|
+
i++;
|
|
17570
|
+
continue;
|
|
17571
|
+
}
|
|
17572
|
+
i++;
|
|
17262
17573
|
}
|
|
17263
|
-
|
|
17264
|
-
|
|
17574
|
+
return tokens;
|
|
17575
|
+
}
|
|
17576
|
+
function isKeyword(token, keywords) {
|
|
17577
|
+
if (!token || token.kind !== "identifier" || token.quoted)
|
|
17578
|
+
return false;
|
|
17579
|
+
return keywords.has(token.value.toUpperCase());
|
|
17580
|
+
}
|
|
17581
|
+
function isPunctuation(token, value) {
|
|
17582
|
+
return token?.kind === "punctuation" && token.value === value;
|
|
17583
|
+
}
|
|
17584
|
+
function readQualifiedName(tokens, index) {
|
|
17585
|
+
const first = tokens[index];
|
|
17586
|
+
if (!first || first.kind !== "identifier")
|
|
17587
|
+
return null;
|
|
17588
|
+
const parts = [first.value];
|
|
17589
|
+
let cursor = index + 1;
|
|
17590
|
+
while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
|
|
17591
|
+
parts.push(tokens[cursor + 1].value);
|
|
17592
|
+
cursor += 2;
|
|
17265
17593
|
}
|
|
17266
|
-
|
|
17267
|
-
|
|
17594
|
+
return { parts, next: cursor };
|
|
17595
|
+
}
|
|
17596
|
+
var ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
|
|
17597
|
+
function decodedVariants(value) {
|
|
17598
|
+
const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
|
|
17599
|
+
return decoded === value ? [] : [decoded];
|
|
17600
|
+
}
|
|
17601
|
+
function fromCodePointOrRaw(hex, whole) {
|
|
17602
|
+
const codePoint = parseInt(hex, 16);
|
|
17603
|
+
return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
|
|
17604
|
+
}
|
|
17605
|
+
function extractTableReferences(sql, options = {}) {
|
|
17606
|
+
const seen = new Set;
|
|
17607
|
+
const references = [];
|
|
17608
|
+
const record = (name) => {
|
|
17609
|
+
const key = name.toLowerCase();
|
|
17610
|
+
if (name.length === 0 || seen.has(key))
|
|
17611
|
+
return;
|
|
17612
|
+
seen.add(key);
|
|
17613
|
+
references.push(name);
|
|
17614
|
+
};
|
|
17615
|
+
const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
|
|
17616
|
+
for (const dialect of dialects) {
|
|
17617
|
+
for (const backslashEscapes of [false, true]) {
|
|
17618
|
+
collectReferences(tokenize(sql, dialect, backslashEscapes), record);
|
|
17619
|
+
}
|
|
17620
|
+
}
|
|
17621
|
+
return references;
|
|
17622
|
+
}
|
|
17623
|
+
function collectReferences(tokens, record) {
|
|
17624
|
+
const recordName = (parts) => {
|
|
17625
|
+
const bare = parts[parts.length - 1];
|
|
17626
|
+
record(bare);
|
|
17627
|
+
for (const variant of decodedVariants(bare))
|
|
17628
|
+
record(variant);
|
|
17629
|
+
if (parts.length > 1)
|
|
17630
|
+
record(parts.join("."));
|
|
17631
|
+
};
|
|
17632
|
+
let i = 0;
|
|
17633
|
+
while (i < tokens.length) {
|
|
17634
|
+
if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
|
|
17635
|
+
i++;
|
|
17636
|
+
continue;
|
|
17637
|
+
}
|
|
17638
|
+
const introducer = tokens[i].value.toUpperCase();
|
|
17639
|
+
const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
|
|
17640
|
+
let cursor = i + 1;
|
|
17641
|
+
while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
|
|
17642
|
+
cursor++;
|
|
17643
|
+
while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
|
|
17644
|
+
cursor++;
|
|
17645
|
+
}
|
|
17646
|
+
let expectTable = true;
|
|
17647
|
+
while (expectTable) {
|
|
17648
|
+
expectTable = false;
|
|
17649
|
+
const name = readQualifiedName(tokens, cursor);
|
|
17650
|
+
if (!name)
|
|
17651
|
+
break;
|
|
17652
|
+
const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
|
|
17653
|
+
if (!isFunctionCall)
|
|
17654
|
+
recordName(name.parts);
|
|
17655
|
+
cursor = name.next;
|
|
17656
|
+
if (isFunctionCall)
|
|
17657
|
+
break;
|
|
17658
|
+
if (isKeyword(tokens[cursor], AS_KEYWORD))
|
|
17659
|
+
cursor++;
|
|
17660
|
+
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
|
|
17661
|
+
cursor++;
|
|
17662
|
+
}
|
|
17663
|
+
while (isPunctuation(tokens[cursor], "(")) {
|
|
17664
|
+
let depth = 0;
|
|
17665
|
+
do {
|
|
17666
|
+
if (isPunctuation(tokens[cursor], "("))
|
|
17667
|
+
depth++;
|
|
17668
|
+
else if (isPunctuation(tokens[cursor], ")"))
|
|
17669
|
+
depth--;
|
|
17670
|
+
cursor++;
|
|
17671
|
+
} while (depth > 0 && cursor < tokens.length);
|
|
17672
|
+
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
|
|
17673
|
+
cursor++;
|
|
17674
|
+
}
|
|
17675
|
+
if (isPunctuation(tokens[cursor], ",")) {
|
|
17676
|
+
cursor++;
|
|
17677
|
+
expectTable = true;
|
|
17678
|
+
}
|
|
17679
|
+
}
|
|
17680
|
+
i = Math.max(cursor, i + 1);
|
|
17268
17681
|
}
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17682
|
+
let index = 0;
|
|
17683
|
+
while (index < tokens.length) {
|
|
17684
|
+
const token = tokens[index];
|
|
17685
|
+
if (!token || token.kind !== "identifier") {
|
|
17686
|
+
index++;
|
|
17687
|
+
continue;
|
|
17688
|
+
}
|
|
17689
|
+
const name = readQualifiedName(tokens, index);
|
|
17690
|
+
for (let part = 0;part < name.parts.length; part++) {
|
|
17691
|
+
const value = name.parts[part];
|
|
17692
|
+
const isQuoted = tokens[index + part * 2]?.quoted === true;
|
|
17693
|
+
if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
|
|
17694
|
+
record(value);
|
|
17695
|
+
for (const variant of decodedVariants(value))
|
|
17696
|
+
record(variant);
|
|
17697
|
+
}
|
|
17698
|
+
}
|
|
17699
|
+
if (name.parts.length > 1)
|
|
17700
|
+
record(name.parts.join("."));
|
|
17701
|
+
index = name.next;
|
|
17273
17702
|
}
|
|
17274
|
-
return options.table || "<unknown-target>";
|
|
17275
17703
|
}
|
|
17276
17704
|
|
|
17277
17705
|
// src/core/limits.ts
|
|
@@ -22684,6 +23112,30 @@ function redactSql(sql) {
|
|
|
22684
23112
|
return redactSensitive(redacted);
|
|
22685
23113
|
}
|
|
22686
23114
|
|
|
23115
|
+
// src/utils/engine-hints.ts
|
|
23116
|
+
var TABLE_NAME_RE = /\b(?:FROM|INTO|UPDATE)\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i;
|
|
23117
|
+
function extractTableName(sql) {
|
|
23118
|
+
const match = sql.match(TABLE_NAME_RE);
|
|
23119
|
+
return match ? match[1] ?? null : null;
|
|
23120
|
+
}
|
|
23121
|
+
function getOperationTarget(system, command, options, sql) {
|
|
23122
|
+
if (system === "mongodb") {
|
|
23123
|
+
return options.collection || "<unknown-collection>";
|
|
23124
|
+
}
|
|
23125
|
+
if (system === "elasticsearch") {
|
|
23126
|
+
return options.index || options.collection || "<unknown-index>";
|
|
23127
|
+
}
|
|
23128
|
+
if (system === "redis") {
|
|
23129
|
+
return options.table || options.key || "<unknown-key>";
|
|
23130
|
+
}
|
|
23131
|
+
if (sql) {
|
|
23132
|
+
const table = extractTableName(sql);
|
|
23133
|
+
if (table)
|
|
23134
|
+
return table;
|
|
23135
|
+
}
|
|
23136
|
+
return options.table || "<unknown-target>";
|
|
23137
|
+
}
|
|
23138
|
+
|
|
22687
23139
|
// src/core/audit/integration-helper.ts
|
|
22688
23140
|
var _sessionIdService = null;
|
|
22689
23141
|
var _loggers = new Map;
|
|
@@ -22885,6 +23337,12 @@ class QueryExecutor {
|
|
|
22885
23337
|
this.config = config;
|
|
22886
23338
|
this.options = options;
|
|
22887
23339
|
}
|
|
23340
|
+
resolveDialect() {
|
|
23341
|
+
if (this.options.dialect)
|
|
23342
|
+
return this.options.dialect;
|
|
23343
|
+
const system = this.config?.connection?.system;
|
|
23344
|
+
return SQL_DIALECTS.find((dialect) => dialect === system);
|
|
23345
|
+
}
|
|
22888
23346
|
takeDiagnostics() {
|
|
22889
23347
|
const diagnostics = this.pendingDiagnostics;
|
|
22890
23348
|
this.pendingDiagnostics = [];
|
|
@@ -22894,7 +23352,7 @@ class QueryExecutor {
|
|
|
22894
23352
|
const start = performance.now();
|
|
22895
23353
|
this.pendingDiagnostics = [];
|
|
22896
23354
|
try {
|
|
22897
|
-
const classification = enforcePermission(sql, this.permission);
|
|
23355
|
+
const classification = enforcePermission(sql, this.permission, this.resolveDialect());
|
|
22898
23356
|
const dangerousOperationWarning = classification.isDangerous && this.permission === "admin" ? `\u26A0 Warning: executing ${classification.type} operation (admin mode)` : undefined;
|
|
22899
23357
|
const AUTO_LIMIT_TYPES = new Set(["SELECT"]);
|
|
22900
23358
|
let executeSql = sql;
|
|
@@ -22911,11 +23369,9 @@ class QueryExecutor {
|
|
|
22911
23369
|
}
|
|
22912
23370
|
}
|
|
22913
23371
|
}
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
|
|
22918
|
-
}
|
|
23372
|
+
const referencedTables = this.blacklistValidator ? extractTableReferences(sql, { dialect: this.resolveDialect() }) : [];
|
|
23373
|
+
if (this.blacklistValidator && referencedTables.length > 0) {
|
|
23374
|
+
this.blacklistValidator.checkTablesBlacklist(classification.type, referencedTables);
|
|
22919
23375
|
}
|
|
22920
23376
|
const resultData = await this.adapter.execute(executeSql);
|
|
22921
23377
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
@@ -22928,15 +23384,12 @@ class QueryExecutor {
|
|
|
22928
23384
|
let securityNotification;
|
|
22929
23385
|
let omittedColumns = [];
|
|
22930
23386
|
if (this.blacklistValidator) {
|
|
22931
|
-
const
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
22938
|
-
securityNotification = this.blacklistValidator.buildSecurityNotification(tableName, filterResult.omittedColumns);
|
|
22939
|
-
}
|
|
23387
|
+
const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
|
|
23388
|
+
filteredRows = filterResult.filteredRows;
|
|
23389
|
+
if (filterResult.omittedColumns.length > 0) {
|
|
23390
|
+
omittedColumns = filterResult.omittedColumns;
|
|
23391
|
+
columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
|
|
23392
|
+
securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
|
|
22940
23393
|
}
|
|
22941
23394
|
}
|
|
22942
23395
|
if (options?.fieldSelection) {
|
|
@@ -23598,6 +24051,14 @@ class BlacklistManager {
|
|
|
23598
24051
|
}
|
|
23599
24052
|
return Array.from(columnSet);
|
|
23600
24053
|
}
|
|
24054
|
+
getAllBlacklistedColumns() {
|
|
24055
|
+
const all = new Set;
|
|
24056
|
+
for (const columnSet of this.state.columns.values()) {
|
|
24057
|
+
for (const column of columnSet)
|
|
24058
|
+
all.add(column);
|
|
24059
|
+
}
|
|
24060
|
+
return Array.from(all);
|
|
24061
|
+
}
|
|
23601
24062
|
canOverrideBlacklist() {
|
|
23602
24063
|
return this.overrideEnabled;
|
|
23603
24064
|
}
|
|
@@ -23605,28 +24066,122 @@ class BlacklistManager {
|
|
|
23605
24066
|
return this.state;
|
|
23606
24067
|
}
|
|
23607
24068
|
}
|
|
24069
|
+
// src/utils/es-index-target.ts
|
|
24070
|
+
var MAX_DECODE_PASSES = 4;
|
|
24071
|
+
function unwrap(target) {
|
|
24072
|
+
let current = target;
|
|
24073
|
+
for (let pass = 0;pass < MAX_DECODE_PASSES; pass++) {
|
|
24074
|
+
let next = current;
|
|
24075
|
+
try {
|
|
24076
|
+
next = decodeURIComponent(next);
|
|
24077
|
+
} catch {}
|
|
24078
|
+
if (next.startsWith("<") && next.endsWith(">")) {
|
|
24079
|
+
next = next.slice(1, -1).replace(/\{[^}]*\}/g, "*");
|
|
24080
|
+
}
|
|
24081
|
+
if (next === current)
|
|
24082
|
+
return current;
|
|
24083
|
+
current = next;
|
|
24084
|
+
}
|
|
24085
|
+
return current;
|
|
24086
|
+
}
|
|
24087
|
+
function expandIndexTargets(target) {
|
|
24088
|
+
const concrete = [];
|
|
24089
|
+
const wildcards = [];
|
|
24090
|
+
const add = (candidate) => {
|
|
24091
|
+
if (candidate.length === 0)
|
|
24092
|
+
return;
|
|
24093
|
+
if (/[*?]/.test(candidate) || candidate.toLowerCase() === "_all")
|
|
24094
|
+
wildcards.push(candidate);
|
|
24095
|
+
else
|
|
24096
|
+
concrete.push(candidate);
|
|
24097
|
+
};
|
|
24098
|
+
for (const rawPart of unwrap(target).split(",")) {
|
|
24099
|
+
const part = unwrap(rawPart.trim()).trim().replace(/^[-+]/, "");
|
|
24100
|
+
if (part.length === 0)
|
|
24101
|
+
continue;
|
|
24102
|
+
add(part);
|
|
24103
|
+
if (part.includes(":"))
|
|
24104
|
+
for (const section of part.split(":"))
|
|
24105
|
+
add(section);
|
|
24106
|
+
}
|
|
24107
|
+
return { concrete, wildcards };
|
|
24108
|
+
}
|
|
24109
|
+
function matchesIndexGlob(pattern, name) {
|
|
24110
|
+
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
|
|
24111
|
+
try {
|
|
24112
|
+
return globToRegex(normalized).test(name.toLowerCase());
|
|
24113
|
+
} catch {
|
|
24114
|
+
return true;
|
|
24115
|
+
}
|
|
24116
|
+
}
|
|
24117
|
+
|
|
23608
24118
|
// src/core/blacklist-validator.ts
|
|
24119
|
+
function dedupe2(values) {
|
|
24120
|
+
const seen = new Set;
|
|
24121
|
+
const result = [];
|
|
24122
|
+
for (const value of values) {
|
|
24123
|
+
const key = value.toLowerCase();
|
|
24124
|
+
if (value.length === 0 || seen.has(key))
|
|
24125
|
+
continue;
|
|
24126
|
+
seen.add(key);
|
|
24127
|
+
result.push(value);
|
|
24128
|
+
}
|
|
24129
|
+
return result;
|
|
24130
|
+
}
|
|
24131
|
+
|
|
23609
24132
|
class BlacklistValidator {
|
|
23610
24133
|
manager;
|
|
23611
24134
|
constructor(manager) {
|
|
23612
24135
|
this.manager = manager;
|
|
23613
24136
|
}
|
|
23614
|
-
checkTableBlacklist(operation, tableName,
|
|
24137
|
+
checkTableBlacklist(operation, tableName, tableList = []) {
|
|
24138
|
+
this.checkTablesBlacklist(operation, [tableName, ...tableList]);
|
|
24139
|
+
}
|
|
24140
|
+
checkTablesBlacklist(operation, tableNames) {
|
|
24141
|
+
const tables = dedupe2(tableNames);
|
|
24142
|
+
if (tables.length === 0) {
|
|
24143
|
+
return;
|
|
24144
|
+
}
|
|
23615
24145
|
if (this.manager.canOverrideBlacklist()) {
|
|
23616
|
-
const
|
|
24146
|
+
const message2 = t_vars("warnings.blacklist_override_used", {
|
|
23617
24147
|
operation,
|
|
23618
|
-
table:
|
|
24148
|
+
table: tables.join(", ")
|
|
23619
24149
|
});
|
|
23620
|
-
console.error(
|
|
24150
|
+
console.error(message2);
|
|
23621
24151
|
return;
|
|
23622
24152
|
}
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
operation
|
|
23627
|
-
});
|
|
23628
|
-
throw new BlacklistError(message, tableName, operation);
|
|
24153
|
+
const blocked = tables.filter((table) => this.manager.isTableBlacklisted(table));
|
|
24154
|
+
if (blocked.length === 0) {
|
|
24155
|
+
return;
|
|
23629
24156
|
}
|
|
24157
|
+
const message = t_vars("errors.table_blacklisted", {
|
|
24158
|
+
table: blocked.join(", "),
|
|
24159
|
+
operation
|
|
24160
|
+
});
|
|
24161
|
+
throw new BlacklistError(message, blocked[0], operation);
|
|
24162
|
+
}
|
|
24163
|
+
checkIndexBlacklist(operation, target) {
|
|
24164
|
+
const { concrete, wildcards } = expandIndexTargets(target);
|
|
24165
|
+
this.checkTablesBlacklist(operation, concrete);
|
|
24166
|
+
if (wildcards.length === 0 || this.manager.canOverrideBlacklist())
|
|
24167
|
+
return;
|
|
24168
|
+
const blacklisted = Array.from(this.manager.getState().tables);
|
|
24169
|
+
if (blacklisted.length === 0)
|
|
24170
|
+
return;
|
|
24171
|
+
const reachable = wildcards.filter((pattern) => blacklisted.some((entry) => matchesIndexGlob(pattern, entry)));
|
|
24172
|
+
if (reachable.length === 0)
|
|
24173
|
+
return;
|
|
24174
|
+
const message = t_vars("errors.table_blacklisted", {
|
|
24175
|
+
table: reachable.join(", "),
|
|
24176
|
+
operation
|
|
24177
|
+
});
|
|
24178
|
+
throw new BlacklistError(message, reachable[0], operation);
|
|
24179
|
+
}
|
|
24180
|
+
filterColumnsForIndexExpression(target, rows, columnList) {
|
|
24181
|
+
const { concrete, wildcards } = expandIndexTargets(target);
|
|
24182
|
+
const ruleKeys = Array.from(this.manager.getState().columns.keys());
|
|
24183
|
+
const reachable = ruleKeys.filter((key) => wildcards.some((pattern) => matchesIndexGlob(pattern, key)));
|
|
24184
|
+
return this.filterColumnsForTables([...concrete, ...reachable], rows, columnList);
|
|
23630
24185
|
}
|
|
23631
24186
|
checkColumnBlacklistOnWrite(tableName, fields, operation = "WRITE") {
|
|
23632
24187
|
const blacklisted = this.manager.getBlacklistedColumns(tableName);
|
|
@@ -23653,7 +24208,11 @@ class BlacklistValidator {
|
|
|
23653
24208
|
throw new BlacklistError(message, tableName, operation);
|
|
23654
24209
|
}
|
|
23655
24210
|
filterColumns(tableName, rows, columnList) {
|
|
23656
|
-
|
|
24211
|
+
return this.filterColumnsForTables([tableName], rows, columnList);
|
|
24212
|
+
}
|
|
24213
|
+
filterColumnsForTables(tableNames, rows, columnList) {
|
|
24214
|
+
const tables = dedupe2(tableNames);
|
|
24215
|
+
const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
|
|
23657
24216
|
if (blacklistedColumns.length === 0) {
|
|
23658
24217
|
return { filteredRows: rows, omittedColumns: [] };
|
|
23659
24218
|
}
|