@carllee1983/dbcli 4.0.0 → 6.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 +78 -0
- package/dist/cli-runtime.mjs +1220 -895
- package/dist/cli.mjs +1 -1
- package/dist/core.d.ts +25 -8
- package/dist/core.mjs +345 -70
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
package/dist/cli-runtime.mjs
CHANGED
|
@@ -51,7 +51,7 @@ var package_default;
|
|
|
51
51
|
var init_package = __esm(() => {
|
|
52
52
|
package_default = {
|
|
53
53
|
name: "@carllee1983/dbcli",
|
|
54
|
-
version: "
|
|
54
|
+
version: "6.0.0",
|
|
55
55
|
description: "Database CLI for AI agents",
|
|
56
56
|
type: "module",
|
|
57
57
|
publishConfig: {
|
|
@@ -4372,7 +4372,7 @@ function validateFormat(value, allowedFormats, commandName) {
|
|
|
4372
4372
|
throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
|
|
4373
4373
|
}
|
|
4374
4374
|
}
|
|
4375
|
-
var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MIN_CONNECTION_TIMEOUT_MS = 100, MAX_CONNECTION_TIMEOUT_MS = 600000, MIN_STATEMENT_TIMEOUT_MS = 0, MAX_STATEMENT_TIMEOUT_MS = 3600000, TimeoutField, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, EnvironmentLabelSchema, MetadataSchema, BLACKLIST_KEY_ALIASES, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionUnion, NamedConnectionSchema, DbcliConfigV2Schema;
|
|
4375
|
+
var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MIN_CONNECTION_TIMEOUT_MS = 100, MAX_CONNECTION_TIMEOUT_MS = 600000, MIN_STATEMENT_TIMEOUT_MS = 0, MAX_STATEMENT_TIMEOUT_MS = 3600000, TimeoutField, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, EnvironmentLabelSchema, MetadataSchema, BLACKLIST_KEY_ALIASES, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, DEFAULT_AUDIT_ROTATION, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionUnion, NamedConnectionSchema, DbcliConfigV2Schema;
|
|
4376
4376
|
var init_validation = __esm(() => {
|
|
4377
4377
|
init_zod();
|
|
4378
4378
|
EnvRefSchema = exports_external.object({
|
|
@@ -4474,10 +4474,11 @@ var init_validation = __esm(() => {
|
|
|
4474
4474
|
RedisConfigSchema = exports_external.object({
|
|
4475
4475
|
mask: exports_external.array(RedisMaskRuleSchema).default([])
|
|
4476
4476
|
}).optional();
|
|
4477
|
+
DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
|
|
4477
4478
|
AuditRotationConfigSchema = exports_external.object({
|
|
4478
|
-
max_bytes: exports_external.number().int().positive().default(
|
|
4479
|
-
max_entries: exports_external.number().int().positive().default(
|
|
4480
|
-
}).optional().default({
|
|
4479
|
+
max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
|
|
4480
|
+
max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
|
|
4481
|
+
}).optional().default({ ...DEFAULT_AUDIT_ROTATION });
|
|
4481
4482
|
AuditConfigSchema = exports_external.object({
|
|
4482
4483
|
enabled: exports_external.boolean().default(true),
|
|
4483
4484
|
strict: exports_external.boolean().default(false),
|
|
@@ -4488,7 +4489,7 @@ var init_validation = __esm(() => {
|
|
|
4488
4489
|
}).optional().default({
|
|
4489
4490
|
enabled: true,
|
|
4490
4491
|
strict: false,
|
|
4491
|
-
rotation: {
|
|
4492
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
4492
4493
|
});
|
|
4493
4494
|
DbcliConfigSchema = exports_external.object({
|
|
4494
4495
|
connection: ConnectionConfigSchema,
|
|
@@ -6321,7 +6322,7 @@ var init_config = __esm(() => {
|
|
|
6321
6322
|
audit: {
|
|
6322
6323
|
enabled: true,
|
|
6323
6324
|
strict: false,
|
|
6324
|
-
rotation: {
|
|
6325
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
6325
6326
|
}
|
|
6326
6327
|
};
|
|
6327
6328
|
configModule = {
|
|
@@ -11164,6 +11165,233 @@ var init_server_side_script = __esm(() => {
|
|
|
11164
11165
|
ES_OPAQUE_BODY_KEYS = ["wrapper"];
|
|
11165
11166
|
});
|
|
11166
11167
|
|
|
11168
|
+
// src/utils/glob.ts
|
|
11169
|
+
function findClassEnd(glob, open2) {
|
|
11170
|
+
for (let i = open2 + 1;i < glob.length; i++) {
|
|
11171
|
+
if (glob[i] === "\\") {
|
|
11172
|
+
i++;
|
|
11173
|
+
continue;
|
|
11174
|
+
}
|
|
11175
|
+
if (glob[i] === "]")
|
|
11176
|
+
return i;
|
|
11177
|
+
}
|
|
11178
|
+
return -1;
|
|
11179
|
+
}
|
|
11180
|
+
function isValidCharacterClass(body) {
|
|
11181
|
+
try {
|
|
11182
|
+
new RegExp(body);
|
|
11183
|
+
return true;
|
|
11184
|
+
} catch {
|
|
11185
|
+
return false;
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
function parseGlob(glob) {
|
|
11189
|
+
const memo = parsedGlobs.get(glob);
|
|
11190
|
+
if (memo !== undefined)
|
|
11191
|
+
return memo;
|
|
11192
|
+
const parsed = parseGlobUncached(glob);
|
|
11193
|
+
parsedGlobs.set(glob, parsed);
|
|
11194
|
+
return parsed;
|
|
11195
|
+
}
|
|
11196
|
+
function parseGlobUncached(glob) {
|
|
11197
|
+
const runs = [];
|
|
11198
|
+
let current = [];
|
|
11199
|
+
let leadingStar = false;
|
|
11200
|
+
let trailingStar = false;
|
|
11201
|
+
let sawStar = false;
|
|
11202
|
+
const endRun = () => {
|
|
11203
|
+
if (current.length > 0)
|
|
11204
|
+
runs.push(current);
|
|
11205
|
+
current = [];
|
|
11206
|
+
};
|
|
11207
|
+
for (let i = 0;i < glob.length; i++) {
|
|
11208
|
+
const c = glob[i];
|
|
11209
|
+
if (c === "\\" && i + 1 < glob.length) {
|
|
11210
|
+
trailingStar = false;
|
|
11211
|
+
current.push({ kind: "literal", char: glob[++i] });
|
|
11212
|
+
continue;
|
|
11213
|
+
}
|
|
11214
|
+
if (c === "*") {
|
|
11215
|
+
if (!sawStar && current.length === 0)
|
|
11216
|
+
leadingStar = true;
|
|
11217
|
+
sawStar = true;
|
|
11218
|
+
trailingStar = true;
|
|
11219
|
+
endRun();
|
|
11220
|
+
continue;
|
|
11221
|
+
}
|
|
11222
|
+
trailingStar = false;
|
|
11223
|
+
if (c === "?") {
|
|
11224
|
+
current.push({ kind: "any" });
|
|
11225
|
+
continue;
|
|
11226
|
+
}
|
|
11227
|
+
if (c === "[") {
|
|
11228
|
+
const end = findClassEnd(glob, i);
|
|
11229
|
+
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
11230
|
+
if (body !== "" && isValidCharacterClass(body)) {
|
|
11231
|
+
current.push({ kind: "class", test: new RegExp(`^${body}$`, "s") });
|
|
11232
|
+
i = end;
|
|
11233
|
+
continue;
|
|
11234
|
+
}
|
|
11235
|
+
current.push({ kind: "literal", char: "[" });
|
|
11236
|
+
continue;
|
|
11237
|
+
}
|
|
11238
|
+
current.push({ kind: "literal", char: c });
|
|
11239
|
+
}
|
|
11240
|
+
endRun();
|
|
11241
|
+
return { runs, leadingStar, trailingStar, anchored: !sawStar };
|
|
11242
|
+
}
|
|
11243
|
+
function runMatchesAt(run, text2, at) {
|
|
11244
|
+
if (at + run.length > text2.length)
|
|
11245
|
+
return false;
|
|
11246
|
+
for (let i = 0;i < run.length; i++) {
|
|
11247
|
+
const token = run[i];
|
|
11248
|
+
const ch = text2[at + i];
|
|
11249
|
+
if (token.kind === "literal") {
|
|
11250
|
+
if (token.char !== ch)
|
|
11251
|
+
return false;
|
|
11252
|
+
} else if (token.kind === "class") {
|
|
11253
|
+
if (!token.test.test(ch))
|
|
11254
|
+
return false;
|
|
11255
|
+
}
|
|
11256
|
+
}
|
|
11257
|
+
return true;
|
|
11258
|
+
}
|
|
11259
|
+
function globMatches(glob, text2) {
|
|
11260
|
+
const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob);
|
|
11261
|
+
if (anchored) {
|
|
11262
|
+
const run = runs[0] ?? [];
|
|
11263
|
+
return text2.length === run.length && runMatchesAt(run, text2, 0);
|
|
11264
|
+
}
|
|
11265
|
+
if (runs.length === 0)
|
|
11266
|
+
return true;
|
|
11267
|
+
let cursor = 0;
|
|
11268
|
+
let first = 0;
|
|
11269
|
+
let last = runs.length;
|
|
11270
|
+
if (!leadingStar) {
|
|
11271
|
+
const head = runs[0];
|
|
11272
|
+
if (!runMatchesAt(head, text2, 0))
|
|
11273
|
+
return false;
|
|
11274
|
+
cursor = head.length;
|
|
11275
|
+
first = 1;
|
|
11276
|
+
}
|
|
11277
|
+
if (!trailingStar) {
|
|
11278
|
+
const tail = runs[last - 1];
|
|
11279
|
+
const at = text2.length - tail.length;
|
|
11280
|
+
if (at < cursor || !runMatchesAt(tail, text2, at))
|
|
11281
|
+
return false;
|
|
11282
|
+
last -= 1;
|
|
11283
|
+
}
|
|
11284
|
+
const limit = trailingStar ? text2.length : text2.length - runs[runs.length - 1].length;
|
|
11285
|
+
for (let r = first;r < last; r++) {
|
|
11286
|
+
const run = runs[r];
|
|
11287
|
+
let found = -1;
|
|
11288
|
+
for (let at = cursor;at + run.length <= limit; at++) {
|
|
11289
|
+
if (runMatchesAt(run, text2, at)) {
|
|
11290
|
+
found = at;
|
|
11291
|
+
break;
|
|
11292
|
+
}
|
|
11293
|
+
}
|
|
11294
|
+
if (found === -1)
|
|
11295
|
+
return false;
|
|
11296
|
+
cursor = found + run.length;
|
|
11297
|
+
}
|
|
11298
|
+
return true;
|
|
11299
|
+
}
|
|
11300
|
+
function escapeGlob(literal) {
|
|
11301
|
+
return literal.replace(/[*?[\]\\]/g, "\\$&");
|
|
11302
|
+
}
|
|
11303
|
+
function globNeverMatches(glob) {
|
|
11304
|
+
for (let i = 0;i < glob.length; i++) {
|
|
11305
|
+
const c = glob[i];
|
|
11306
|
+
if (c === "\\") {
|
|
11307
|
+
i++;
|
|
11308
|
+
continue;
|
|
11309
|
+
}
|
|
11310
|
+
if (c !== "[")
|
|
11311
|
+
continue;
|
|
11312
|
+
const end = findClassEnd(glob, i);
|
|
11313
|
+
if (end === -1) {
|
|
11314
|
+
return "unclosed character class";
|
|
11315
|
+
}
|
|
11316
|
+
const body = glob.slice(i, end + 1);
|
|
11317
|
+
if (body === "[]" || body === "[^]")
|
|
11318
|
+
return "empty character class matches nothing";
|
|
11319
|
+
if (!isValidCharacterClass(body))
|
|
11320
|
+
return "invalid character class";
|
|
11321
|
+
i = end;
|
|
11322
|
+
}
|
|
11323
|
+
return null;
|
|
11324
|
+
}
|
|
11325
|
+
var parsedGlobs;
|
|
11326
|
+
var init_glob = __esm(() => {
|
|
11327
|
+
parsedGlobs = new Map;
|
|
11328
|
+
});
|
|
11329
|
+
|
|
11330
|
+
// src/core/mongo/path-matcher.ts
|
|
11331
|
+
function compilePatterns(raw) {
|
|
11332
|
+
const patterns = [];
|
|
11333
|
+
const rejected = [];
|
|
11334
|
+
for (const entry of raw) {
|
|
11335
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
11336
|
+
rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
|
|
11337
|
+
continue;
|
|
11338
|
+
}
|
|
11339
|
+
const segments = entry.split(".");
|
|
11340
|
+
if (segments.some((s) => s.length === 0)) {
|
|
11341
|
+
rejected.push({ raw: entry, reason: "empty path segment" });
|
|
11342
|
+
continue;
|
|
11343
|
+
}
|
|
11344
|
+
const globstar = segments.find((seg) => seg.includes("**"));
|
|
11345
|
+
if (globstar !== undefined) {
|
|
11346
|
+
rejected.push({
|
|
11347
|
+
raw: entry,
|
|
11348
|
+
reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
|
|
11349
|
+
});
|
|
11350
|
+
continue;
|
|
11351
|
+
}
|
|
11352
|
+
const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
|
|
11353
|
+
if (dead !== undefined && dead !== null) {
|
|
11354
|
+
rejected.push({ raw: entry, reason: dead });
|
|
11355
|
+
continue;
|
|
11356
|
+
}
|
|
11357
|
+
const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
|
|
11358
|
+
const literal = wildcardTail ? segments.slice(0, -1) : segments;
|
|
11359
|
+
patterns.push({
|
|
11360
|
+
raw: entry,
|
|
11361
|
+
segments: literal,
|
|
11362
|
+
wildcardTail
|
|
11363
|
+
});
|
|
11364
|
+
}
|
|
11365
|
+
return { patterns, rejected };
|
|
11366
|
+
}
|
|
11367
|
+
function matchAny(path, patterns) {
|
|
11368
|
+
if (patterns.length === 0)
|
|
11369
|
+
return false;
|
|
11370
|
+
const pathSegments = path.split(".");
|
|
11371
|
+
for (const pat of patterns) {
|
|
11372
|
+
if (pat.wildcardTail) {
|
|
11373
|
+
if (pathSegments.length < pat.segments.length)
|
|
11374
|
+
continue;
|
|
11375
|
+
} else {
|
|
11376
|
+
if (pat.segments.length !== pathSegments.length)
|
|
11377
|
+
continue;
|
|
11378
|
+
}
|
|
11379
|
+
let ok = true;
|
|
11380
|
+
for (let i = 0;i < pat.segments.length; i++) {
|
|
11381
|
+
if (!globMatches(pat.segments[i], pathSegments[i])) {
|
|
11382
|
+
ok = false;
|
|
11383
|
+
break;
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
if (ok)
|
|
11387
|
+
return true;
|
|
11388
|
+
}
|
|
11389
|
+
return false;
|
|
11390
|
+
}
|
|
11391
|
+
var init_path_matcher = __esm(() => {
|
|
11392
|
+
init_glob();
|
|
11393
|
+
});
|
|
11394
|
+
|
|
11167
11395
|
// src/core/mongo/request-fields.ts
|
|
11168
11396
|
function asFieldPath(text2) {
|
|
11169
11397
|
if (text2.startsWith("$$"))
|
|
@@ -11172,15 +11400,31 @@ function asFieldPath(text2) {
|
|
|
11172
11400
|
return text2.slice(1);
|
|
11173
11401
|
return text2;
|
|
11174
11402
|
}
|
|
11175
|
-
function
|
|
11403
|
+
function globRulesOf(protectedFields) {
|
|
11404
|
+
const globbed = [...protectedFields].filter((rule) => /[*?[\\]/.test(rule));
|
|
11405
|
+
if (globbed.length === 0)
|
|
11406
|
+
return [];
|
|
11407
|
+
const { patterns, rejected } = compilePatterns(globbed);
|
|
11408
|
+
if (rejected.length > 0) {
|
|
11409
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
11410
|
+
throw new Error(`BlacklistRejection: blacklist entries this matcher cannot read: ${detail}`);
|
|
11411
|
+
}
|
|
11412
|
+
return patterns;
|
|
11413
|
+
}
|
|
11414
|
+
function reachesProtectedField(path, protectedFields, globs) {
|
|
11176
11415
|
if (protectedFields.has(path))
|
|
11177
11416
|
return true;
|
|
11417
|
+
if (globs.length > 0 && matchAny(path, globs))
|
|
11418
|
+
return true;
|
|
11178
11419
|
const parts = path.split(".");
|
|
11179
11420
|
if (parts.length === 1)
|
|
11180
11421
|
return false;
|
|
11181
11422
|
for (let start = 0;start < parts.length; start += 1) {
|
|
11182
11423
|
for (let end = start + 1;end <= parts.length; end += 1) {
|
|
11183
|
-
|
|
11424
|
+
const candidate = parts.slice(start, end).join(".");
|
|
11425
|
+
if (protectedFields.has(candidate))
|
|
11426
|
+
return true;
|
|
11427
|
+
if (globs.length > 0 && matchAny(candidate, globs))
|
|
11184
11428
|
return true;
|
|
11185
11429
|
}
|
|
11186
11430
|
}
|
|
@@ -11189,6 +11433,7 @@ function reachesProtectedField(path, protectedFields) {
|
|
|
11189
11433
|
function findProtectedFieldReference(request, protectedFields) {
|
|
11190
11434
|
if (protectedFields.size === 0)
|
|
11191
11435
|
return;
|
|
11436
|
+
const globs = globRulesOf(protectedFields);
|
|
11192
11437
|
const candidates = [];
|
|
11193
11438
|
let movesWholeDocument = false;
|
|
11194
11439
|
const walk = (node) => {
|
|
@@ -11222,7 +11467,7 @@ function findProtectedFieldReference(request, protectedFields) {
|
|
|
11222
11467
|
}
|
|
11223
11468
|
};
|
|
11224
11469
|
walk(request);
|
|
11225
|
-
const named = candidates.find((path) => path.length > 0 && reachesProtectedField(path, protectedFields));
|
|
11470
|
+
const named = candidates.find((path) => path.length > 0 && reachesProtectedField(path, protectedFields, globs));
|
|
11226
11471
|
if (named !== undefined)
|
|
11227
11472
|
return named;
|
|
11228
11473
|
return movesWholeDocument ? [...protectedFields][0] : undefined;
|
|
@@ -11261,6 +11506,7 @@ function protectedFieldsForRequest(request, collection, columns) {
|
|
|
11261
11506
|
}
|
|
11262
11507
|
var WHOLE_DOCUMENT_VARIABLES, RESHAPES_DOCUMENT, NAMES_A_FIELD_DYNAMICALLY = "$getField";
|
|
11263
11508
|
var init_request_fields = __esm(() => {
|
|
11509
|
+
init_path_matcher();
|
|
11264
11510
|
WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
|
|
11265
11511
|
RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
|
|
11266
11512
|
});
|
|
@@ -12215,66 +12461,13 @@ var init_size_guard = __esm(() => {
|
|
|
12215
12461
|
init_types3();
|
|
12216
12462
|
});
|
|
12217
12463
|
|
|
12218
|
-
// src/utils/glob.ts
|
|
12219
|
-
function globToRegex(glob) {
|
|
12220
|
-
let out = "^";
|
|
12221
|
-
for (let i = 0;i < glob.length; i++) {
|
|
12222
|
-
const c = glob[i];
|
|
12223
|
-
if (c === "\\" && i + 1 < glob.length) {
|
|
12224
|
-
out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12225
|
-
continue;
|
|
12226
|
-
}
|
|
12227
|
-
if (c === "*")
|
|
12228
|
-
out += ".*";
|
|
12229
|
-
else if (c === "?")
|
|
12230
|
-
out += ".";
|
|
12231
|
-
else if (c === "[") {
|
|
12232
|
-
const end = findClassEnd(glob, i);
|
|
12233
|
-
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
12234
|
-
if (body !== "" && isValidCharacterClass(body)) {
|
|
12235
|
-
out += body;
|
|
12236
|
-
i = end;
|
|
12237
|
-
} else {
|
|
12238
|
-
out += "\\[";
|
|
12239
|
-
}
|
|
12240
|
-
} else if (".^$+(){}|\\".includes(c)) {
|
|
12241
|
-
out += "\\" + c;
|
|
12242
|
-
} else {
|
|
12243
|
-
out += c;
|
|
12244
|
-
}
|
|
12245
|
-
}
|
|
12246
|
-
out += "$";
|
|
12247
|
-
return new RegExp(out);
|
|
12248
|
-
}
|
|
12249
|
-
function findClassEnd(glob, open2) {
|
|
12250
|
-
for (let i = open2 + 1;i < glob.length; i++) {
|
|
12251
|
-
if (glob[i] === "\\") {
|
|
12252
|
-
i++;
|
|
12253
|
-
continue;
|
|
12254
|
-
}
|
|
12255
|
-
if (glob[i] === "]")
|
|
12256
|
-
return i;
|
|
12257
|
-
}
|
|
12258
|
-
return -1;
|
|
12259
|
-
}
|
|
12260
|
-
function isValidCharacterClass(body) {
|
|
12261
|
-
try {
|
|
12262
|
-
new RegExp(body);
|
|
12263
|
-
return true;
|
|
12264
|
-
} catch {
|
|
12265
|
-
return false;
|
|
12266
|
-
}
|
|
12267
|
-
}
|
|
12268
|
-
|
|
12269
12464
|
// src/adapters/redis/blacklist-enforcer.ts
|
|
12270
12465
|
function patternsOverlap(a, b) {
|
|
12271
|
-
const ra = globToRegex(a);
|
|
12272
|
-
const rb = globToRegex(b);
|
|
12273
12466
|
const probeA = sampleFromGlob(a);
|
|
12274
12467
|
const probeB = sampleFromGlob(b);
|
|
12275
|
-
if (probeA !== null &&
|
|
12468
|
+
if (probeA !== null && globMatches(b, probeA))
|
|
12276
12469
|
return true;
|
|
12277
|
-
if (probeB !== null &&
|
|
12470
|
+
if (probeB !== null && globMatches(a, probeB))
|
|
12278
12471
|
return true;
|
|
12279
12472
|
return false;
|
|
12280
12473
|
}
|
|
@@ -12317,7 +12510,7 @@ function checkKeyArgs(command, args, rules) {
|
|
|
12317
12510
|
if (key === undefined)
|
|
12318
12511
|
continue;
|
|
12319
12512
|
for (const pat of rules) {
|
|
12320
|
-
if (
|
|
12513
|
+
if (globMatches(pat, key)) {
|
|
12321
12514
|
return { ok: false, matchedKey: key, matchedPattern: pat };
|
|
12322
12515
|
}
|
|
12323
12516
|
}
|
|
@@ -12374,6 +12567,7 @@ function expandKeyArity(arity, args, argCount) {
|
|
|
12374
12567
|
}
|
|
12375
12568
|
var init_blacklist_enforcer = __esm(() => {
|
|
12376
12569
|
init_command_metadata();
|
|
12570
|
+
init_glob();
|
|
12377
12571
|
});
|
|
12378
12572
|
|
|
12379
12573
|
// src/adapters/redis/returned-key-names.ts
|
|
@@ -12385,8 +12579,7 @@ function filterReturnedKeyNames(command, reply, rules) {
|
|
|
12385
12579
|
return reply;
|
|
12386
12580
|
if (!returnsKeyNames(command))
|
|
12387
12581
|
return reply;
|
|
12388
|
-
const
|
|
12389
|
-
const permitted = (key) => typeof key === "string" && regexes.every((regex) => !regex.test(key));
|
|
12582
|
+
const permitted = (key) => typeof key === "string" && rules.every((rule) => !globMatches(rule, key));
|
|
12390
12583
|
if (Array.isArray(reply) && reply.length === 2 && Array.isArray(reply[1])) {
|
|
12391
12584
|
return [reply[0], reply[1].filter(permitted)];
|
|
12392
12585
|
}
|
|
@@ -12396,12 +12589,13 @@ function filterReturnedKeyNames(command, reply, rules) {
|
|
|
12396
12589
|
}
|
|
12397
12590
|
var RETURNS_KEY_NAMES;
|
|
12398
12591
|
var init_returned_key_names = __esm(() => {
|
|
12592
|
+
init_glob();
|
|
12399
12593
|
RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
12400
12594
|
});
|
|
12401
12595
|
|
|
12402
12596
|
// src/adapters/redis/value-masker.ts
|
|
12403
12597
|
function planFor(key, rules) {
|
|
12404
|
-
const matched = rules.filter((r) =>
|
|
12598
|
+
const matched = rules.filter((r) => globMatches(r.keyPattern, key));
|
|
12405
12599
|
if (matched.length === 0)
|
|
12406
12600
|
return null;
|
|
12407
12601
|
const wholeValue = matched.some((r) => !r.fields || r.fields.length === 0);
|
|
@@ -12456,7 +12650,7 @@ function maskRedisRows(command, args, rows, rules) {
|
|
|
12456
12650
|
}
|
|
12457
12651
|
var REDACTED = "[REDACTED]", MASKABLE;
|
|
12458
12652
|
var init_value_masker = __esm(() => {
|
|
12459
|
-
|
|
12653
|
+
init_glob();
|
|
12460
12654
|
MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
|
|
12461
12655
|
});
|
|
12462
12656
|
|
|
@@ -12547,8 +12741,7 @@ class RedisAdapter {
|
|
|
12547
12741
|
async sampleKeyNames(limit) {
|
|
12548
12742
|
const client = this.requireClient();
|
|
12549
12743
|
const rules = this.blacklistRules;
|
|
12550
|
-
const
|
|
12551
|
-
const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => regexes.every((r) => !r.test(key)));
|
|
12744
|
+
const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => rules.every((rule) => !globMatches(rule, key)));
|
|
12552
12745
|
return { names: keys, truncated: scanned >= limit };
|
|
12553
12746
|
}
|
|
12554
12747
|
async getDbSize() {
|
|
@@ -12927,6 +13120,7 @@ var init_redis_adapter = __esm(() => {
|
|
|
12927
13120
|
init_types2();
|
|
12928
13121
|
init_size_guard();
|
|
12929
13122
|
init_blacklist_enforcer();
|
|
13123
|
+
init_glob();
|
|
12930
13124
|
init_returned_key_names();
|
|
12931
13125
|
init_types3();
|
|
12932
13126
|
init_command_metadata();
|
|
@@ -13449,7 +13643,7 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
13449
13643
|
audit: {
|
|
13450
13644
|
enabled: true,
|
|
13451
13645
|
strict: false,
|
|
13452
|
-
rotation: {
|
|
13646
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
13453
13647
|
}
|
|
13454
13648
|
};
|
|
13455
13649
|
await writeV2Config(storagePath, v2Config);
|
|
@@ -13464,6 +13658,7 @@ var init_init_shared = __esm(() => {
|
|
|
13464
13658
|
init_config();
|
|
13465
13659
|
init_config_v2();
|
|
13466
13660
|
init_prompts();
|
|
13661
|
+
init_validation();
|
|
13467
13662
|
init_config_binding();
|
|
13468
13663
|
});
|
|
13469
13664
|
|
|
@@ -16457,6 +16652,23 @@ class ColumnIndexBuilder {
|
|
|
16457
16652
|
}
|
|
16458
16653
|
}
|
|
16459
16654
|
// src/core/blacklist-manager.ts
|
|
16655
|
+
function normalizeBlacklistEntry(raw) {
|
|
16656
|
+
const trimmed = raw.trim();
|
|
16657
|
+
const quote = trimmed[0];
|
|
16658
|
+
if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
|
|
16659
|
+
return trimmed.slice(1, -1).trim();
|
|
16660
|
+
}
|
|
16661
|
+
return trimmed;
|
|
16662
|
+
}
|
|
16663
|
+
function assertNotTableQualified(tableKey, column) {
|
|
16664
|
+
const dot = column.indexOf(".");
|
|
16665
|
+
if (dot <= 0)
|
|
16666
|
+
return;
|
|
16667
|
+
if (column.slice(0, dot).toLowerCase() !== tableKey.toLowerCase())
|
|
16668
|
+
return;
|
|
16669
|
+
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))}.`);
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16460
16672
|
class BlacklistManager {
|
|
16461
16673
|
config;
|
|
16462
16674
|
state;
|
|
@@ -16465,6 +16677,7 @@ class BlacklistManager {
|
|
|
16465
16677
|
this.config = config;
|
|
16466
16678
|
this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
|
|
16467
16679
|
this.state = this.loadBlacklist();
|
|
16680
|
+
this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
|
|
16468
16681
|
}
|
|
16469
16682
|
loadBlacklist() {
|
|
16470
16683
|
const tables = new Set;
|
|
@@ -16476,7 +16689,7 @@ class BlacklistManager {
|
|
|
16476
16689
|
if (Array.isArray(blacklistConfig.tables)) {
|
|
16477
16690
|
for (const tableName of blacklistConfig.tables) {
|
|
16478
16691
|
if (typeof tableName === "string") {
|
|
16479
|
-
tables.add(tableName.toLowerCase());
|
|
16692
|
+
tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
|
|
16480
16693
|
} else {
|
|
16481
16694
|
console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
|
|
16482
16695
|
}
|
|
@@ -16497,13 +16710,15 @@ class BlacklistManager {
|
|
|
16497
16710
|
const columnSet = new Set;
|
|
16498
16711
|
for (const col of cols) {
|
|
16499
16712
|
if (typeof col === "string") {
|
|
16500
|
-
|
|
16713
|
+
const entry = normalizeBlacklistEntry(col);
|
|
16714
|
+
assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
|
|
16715
|
+
columnSet.add(entry);
|
|
16501
16716
|
} else {
|
|
16502
16717
|
console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
|
|
16503
16718
|
}
|
|
16504
16719
|
}
|
|
16505
16720
|
if (columnSet.size > 0) {
|
|
16506
|
-
columns.set(tableName.toLowerCase(), columnSet);
|
|
16721
|
+
columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
|
|
16507
16722
|
}
|
|
16508
16723
|
}
|
|
16509
16724
|
} else if (blacklistConfig.columns !== undefined) {
|
|
@@ -16512,17 +16727,47 @@ class BlacklistManager {
|
|
|
16512
16727
|
return { tables, columns };
|
|
16513
16728
|
}
|
|
16514
16729
|
isTableBlacklisted(tableName) {
|
|
16515
|
-
|
|
16730
|
+
const name = tableName.toLowerCase();
|
|
16731
|
+
if (this.state.tables.has(name))
|
|
16732
|
+
return true;
|
|
16733
|
+
for (const pattern of this.wildcardTables) {
|
|
16734
|
+
if (globMatches(pattern, name))
|
|
16735
|
+
return true;
|
|
16736
|
+
}
|
|
16737
|
+
return false;
|
|
16738
|
+
}
|
|
16739
|
+
wildcardTables;
|
|
16740
|
+
columnRulesFor(tableName) {
|
|
16741
|
+
const key = normalizeBlacklistEntry(tableName).toLowerCase();
|
|
16742
|
+
const direct = this.state.columns.get(key);
|
|
16743
|
+
if (direct)
|
|
16744
|
+
return direct;
|
|
16745
|
+
const dot = key.lastIndexOf(".");
|
|
16746
|
+
if (dot > 0) {
|
|
16747
|
+
const bare = this.state.columns.get(key.slice(dot + 1));
|
|
16748
|
+
if (bare)
|
|
16749
|
+
return bare;
|
|
16750
|
+
}
|
|
16751
|
+
let merged;
|
|
16752
|
+
for (const [ruleKey, columns] of this.state.columns) {
|
|
16753
|
+
const ruleDot = ruleKey.lastIndexOf(".");
|
|
16754
|
+
if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
|
|
16755
|
+
continue;
|
|
16756
|
+
merged ??= new Set;
|
|
16757
|
+
for (const column of columns)
|
|
16758
|
+
merged.add(column);
|
|
16759
|
+
}
|
|
16760
|
+
return merged;
|
|
16516
16761
|
}
|
|
16517
16762
|
isColumnBlacklisted(tableName, columnName) {
|
|
16518
|
-
const columnSet = this.
|
|
16763
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
16519
16764
|
if (!columnSet) {
|
|
16520
16765
|
return false;
|
|
16521
16766
|
}
|
|
16522
|
-
return columnSet.has(columnName);
|
|
16767
|
+
return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
|
|
16523
16768
|
}
|
|
16524
16769
|
getBlacklistedColumns(tableName) {
|
|
16525
|
-
const columnSet = this.
|
|
16770
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
16526
16771
|
if (!columnSet) {
|
|
16527
16772
|
return [];
|
|
16528
16773
|
}
|
|
@@ -16543,6 +16788,9 @@ class BlacklistManager {
|
|
|
16543
16788
|
return this.state;
|
|
16544
16789
|
}
|
|
16545
16790
|
}
|
|
16791
|
+
var init_blacklist_manager = __esm(() => {
|
|
16792
|
+
init_glob();
|
|
16793
|
+
});
|
|
16546
16794
|
|
|
16547
16795
|
// src/types/blacklist.ts
|
|
16548
16796
|
var BlacklistError;
|
|
@@ -16819,7 +17067,7 @@ function expandIndexTargets(target) {
|
|
|
16819
17067
|
function matchesIndexGlob(pattern, name) {
|
|
16820
17068
|
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
|
|
16821
17069
|
try {
|
|
16822
|
-
return
|
|
17070
|
+
return globMatches(normalized, name.toLowerCase());
|
|
16823
17071
|
} catch {
|
|
16824
17072
|
return true;
|
|
16825
17073
|
}
|
|
@@ -16851,7 +17099,9 @@ function indexExpressionReaches(expression, blacklisted) {
|
|
|
16851
17099
|
return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
|
|
16852
17100
|
}
|
|
16853
17101
|
var MAX_DECODE_PASSES = 4;
|
|
16854
|
-
var init_es_index_target = () => {
|
|
17102
|
+
var init_es_index_target = __esm(() => {
|
|
17103
|
+
init_glob();
|
|
17104
|
+
});
|
|
16855
17105
|
|
|
16856
17106
|
// src/core/blacklist-validator.ts
|
|
16857
17107
|
function flattenArrayRow(row) {
|
|
@@ -16876,6 +17126,17 @@ function dedupe2(values) {
|
|
|
16876
17126
|
}
|
|
16877
17127
|
return result;
|
|
16878
17128
|
}
|
|
17129
|
+
function compileGlobRules(rules, fold, table, operation) {
|
|
17130
|
+
const globbed = rules.filter((rule) => /[*?[\\]/.test(rule));
|
|
17131
|
+
if (globbed.length === 0)
|
|
17132
|
+
return [];
|
|
17133
|
+
const { patterns, rejected } = compilePatterns(globbed.map(fold));
|
|
17134
|
+
if (rejected.length > 0) {
|
|
17135
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
17136
|
+
throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
|
|
17137
|
+
}
|
|
17138
|
+
return patterns;
|
|
17139
|
+
}
|
|
16879
17140
|
|
|
16880
17141
|
class BlacklistValidator {
|
|
16881
17142
|
manager;
|
|
@@ -16933,7 +17194,27 @@ class BlacklistValidator {
|
|
|
16933
17194
|
if (blacklisted.length === 0 || fields.length === 0) {
|
|
16934
17195
|
return;
|
|
16935
17196
|
}
|
|
16936
|
-
const
|
|
17197
|
+
const protectedPaths = new Set(blacklisted.map((name) => name.toLowerCase()));
|
|
17198
|
+
const globs = compileGlobRules(blacklisted, (value) => value.toLowerCase(), tableName, operation);
|
|
17199
|
+
const conflicts = fields.filter((field) => {
|
|
17200
|
+
const name = field.toLowerCase();
|
|
17201
|
+
if (protectedPaths.has(name))
|
|
17202
|
+
return true;
|
|
17203
|
+
if (matchAny(name, globs))
|
|
17204
|
+
return true;
|
|
17205
|
+
let dot = name.indexOf(".");
|
|
17206
|
+
while (dot >= 0) {
|
|
17207
|
+
if (dot > 0) {
|
|
17208
|
+
const ancestor = name.slice(0, dot);
|
|
17209
|
+
if (protectedPaths.has(ancestor))
|
|
17210
|
+
return true;
|
|
17211
|
+
if (matchAny(ancestor, globs))
|
|
17212
|
+
return true;
|
|
17213
|
+
}
|
|
17214
|
+
dot = name.indexOf(".", dot + 1);
|
|
17215
|
+
}
|
|
17216
|
+
return false;
|
|
17217
|
+
});
|
|
16937
17218
|
if (conflicts.length === 0) {
|
|
16938
17219
|
return;
|
|
16939
17220
|
}
|
|
@@ -16984,11 +17265,20 @@ class BlacklistValidator {
|
|
|
16984
17265
|
}
|
|
16985
17266
|
collect(row);
|
|
16986
17267
|
}
|
|
16987
|
-
const
|
|
17268
|
+
const foldHead = (path) => {
|
|
17269
|
+
const dot = path.indexOf(".");
|
|
17270
|
+
return dot < 0 ? path.toLowerCase() : path.slice(0, dot).toLowerCase() + path.slice(dot);
|
|
17271
|
+
};
|
|
17272
|
+
const protectedPaths = new Set(blacklistedColumns.map(foldHead));
|
|
17273
|
+
const globRules = compileGlobRules(blacklistedColumns, foldHead, tables[0] ?? "unknown", "SELECT");
|
|
17274
|
+
const presentByFolded = new Map;
|
|
17275
|
+
for (const column of presentColumns)
|
|
17276
|
+
presentByFolded.set(foldHead(column), column);
|
|
16988
17277
|
const omitted = new Set;
|
|
16989
17278
|
for (const path of blacklistedColumns) {
|
|
16990
|
-
|
|
16991
|
-
|
|
17279
|
+
const present = presentByFolded.get(foldHead(path));
|
|
17280
|
+
if (present !== undefined) {
|
|
17281
|
+
omitted.add(present);
|
|
16992
17282
|
continue;
|
|
16993
17283
|
}
|
|
16994
17284
|
const dot = path.indexOf(".");
|
|
@@ -17003,13 +17293,30 @@ class BlacklistValidator {
|
|
|
17003
17293
|
for (const column of presentColumns) {
|
|
17004
17294
|
let dot = column.indexOf(".");
|
|
17005
17295
|
while (dot >= 0) {
|
|
17006
|
-
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
17296
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot).toLowerCase())) {
|
|
17007
17297
|
omitted.add(column);
|
|
17008
17298
|
break;
|
|
17009
17299
|
}
|
|
17010
17300
|
dot = column.indexOf(".", dot + 1);
|
|
17011
17301
|
}
|
|
17012
17302
|
}
|
|
17303
|
+
if (globRules.length > 0) {
|
|
17304
|
+
for (const column of presentColumns) {
|
|
17305
|
+
const folded = foldHead(column);
|
|
17306
|
+
if (matchAny(folded, globRules)) {
|
|
17307
|
+
omitted.add(column);
|
|
17308
|
+
continue;
|
|
17309
|
+
}
|
|
17310
|
+
let dot = folded.indexOf(".");
|
|
17311
|
+
while (dot >= 0) {
|
|
17312
|
+
if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
|
|
17313
|
+
omitted.add(column);
|
|
17314
|
+
break;
|
|
17315
|
+
}
|
|
17316
|
+
dot = folded.indexOf(".", dot + 1);
|
|
17317
|
+
}
|
|
17318
|
+
}
|
|
17319
|
+
}
|
|
17013
17320
|
const omittedColumns = Array.from(omitted);
|
|
17014
17321
|
if (omittedColumns.length === 0) {
|
|
17015
17322
|
return { filteredRows: rows, omittedColumns: [] };
|
|
@@ -17030,6 +17337,7 @@ var init_blacklist_validator = __esm(() => {
|
|
|
17030
17337
|
init_blacklist();
|
|
17031
17338
|
init_message_loader();
|
|
17032
17339
|
init_field_projection();
|
|
17340
|
+
init_path_matcher();
|
|
17033
17341
|
init_es_index_target();
|
|
17034
17342
|
});
|
|
17035
17343
|
|
|
@@ -18040,6 +18348,7 @@ var init_core = __esm(() => {
|
|
|
18040
18348
|
init_schema_updater();
|
|
18041
18349
|
init_concurrent_lock();
|
|
18042
18350
|
init_error_recovery();
|
|
18351
|
+
init_blacklist_manager();
|
|
18043
18352
|
init_blacklist_validator();
|
|
18044
18353
|
init_size_category();
|
|
18045
18354
|
init_health_checker();
|
|
@@ -18418,6 +18727,355 @@ var init_sql_lexical = __esm(() => {
|
|
|
18418
18727
|
DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
|
|
18419
18728
|
});
|
|
18420
18729
|
|
|
18730
|
+
// src/utils/sql-tables.ts
|
|
18731
|
+
var exports_sql_tables = {};
|
|
18732
|
+
__export(exports_sql_tables, {
|
|
18733
|
+
extractTableReferences: () => extractTableReferences
|
|
18734
|
+
});
|
|
18735
|
+
function tokenize(sql, dialect, backslashEscapes) {
|
|
18736
|
+
const tokens = [];
|
|
18737
|
+
const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
|
|
18738
|
+
let i = 0;
|
|
18739
|
+
while (i < sql.length) {
|
|
18740
|
+
const char = sql[i];
|
|
18741
|
+
const dashFollowerCode = sql.charCodeAt(i + 2);
|
|
18742
|
+
if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
|
|
18743
|
+
while (i < sql.length && sql[i] !== `
|
|
18744
|
+
`)
|
|
18745
|
+
i++;
|
|
18746
|
+
continue;
|
|
18747
|
+
}
|
|
18748
|
+
if (mysqlDialect && char === "#") {
|
|
18749
|
+
while (i < sql.length && sql[i] !== `
|
|
18750
|
+
`)
|
|
18751
|
+
i++;
|
|
18752
|
+
continue;
|
|
18753
|
+
}
|
|
18754
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
18755
|
+
if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
|
|
18756
|
+
const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
|
|
18757
|
+
const closingIndex = sql.indexOf("*/", i + prefixLength);
|
|
18758
|
+
const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
|
|
18759
|
+
const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
|
|
18760
|
+
tokens.push(...tokenize(body, dialect, backslashEscapes));
|
|
18761
|
+
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
18762
|
+
continue;
|
|
18763
|
+
}
|
|
18764
|
+
const nests = dialect === "postgresql";
|
|
18765
|
+
let depth = 1;
|
|
18766
|
+
i += 2;
|
|
18767
|
+
while (i < sql.length && depth > 0) {
|
|
18768
|
+
if (nests && sql[i] === "/" && sql[i + 1] === "*") {
|
|
18769
|
+
depth++;
|
|
18770
|
+
i += 2;
|
|
18771
|
+
continue;
|
|
18772
|
+
}
|
|
18773
|
+
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
18774
|
+
depth--;
|
|
18775
|
+
i += 2;
|
|
18776
|
+
continue;
|
|
18777
|
+
}
|
|
18778
|
+
i++;
|
|
18779
|
+
}
|
|
18780
|
+
continue;
|
|
18781
|
+
}
|
|
18782
|
+
if (dialect === "postgresql" && char === "$") {
|
|
18783
|
+
const delimiter = dollarQuoteDelimiterAt(sql, i);
|
|
18784
|
+
if (delimiter) {
|
|
18785
|
+
i += delimiter.length;
|
|
18786
|
+
const closingIndex = sql.indexOf(delimiter, i);
|
|
18787
|
+
i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
|
|
18788
|
+
continue;
|
|
18789
|
+
}
|
|
18790
|
+
}
|
|
18791
|
+
if (char === "'") {
|
|
18792
|
+
i++;
|
|
18793
|
+
while (i < sql.length) {
|
|
18794
|
+
if (sql[i] === "'") {
|
|
18795
|
+
if (sql[i + 1] === "'") {
|
|
18796
|
+
i += 2;
|
|
18797
|
+
continue;
|
|
18798
|
+
}
|
|
18799
|
+
i++;
|
|
18800
|
+
break;
|
|
18801
|
+
}
|
|
18802
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
18803
|
+
i += 2;
|
|
18804
|
+
continue;
|
|
18805
|
+
}
|
|
18806
|
+
i++;
|
|
18807
|
+
}
|
|
18808
|
+
continue;
|
|
18809
|
+
}
|
|
18810
|
+
if (char === '"' || char === "`") {
|
|
18811
|
+
const quote = char;
|
|
18812
|
+
i++;
|
|
18813
|
+
let value = "";
|
|
18814
|
+
while (i < sql.length) {
|
|
18815
|
+
if (sql[i] === quote) {
|
|
18816
|
+
if (sql[i + 1] === quote) {
|
|
18817
|
+
value += quote;
|
|
18818
|
+
i += 2;
|
|
18819
|
+
continue;
|
|
18820
|
+
}
|
|
18821
|
+
i++;
|
|
18822
|
+
break;
|
|
18823
|
+
}
|
|
18824
|
+
if (backslashEscapes && sql[i] === "\\") {
|
|
18825
|
+
value += sql[i + 1] ?? "";
|
|
18826
|
+
i += 2;
|
|
18827
|
+
continue;
|
|
18828
|
+
}
|
|
18829
|
+
value += sql[i];
|
|
18830
|
+
i++;
|
|
18831
|
+
}
|
|
18832
|
+
tokens.push({ value, kind: "identifier", quoted: true });
|
|
18833
|
+
continue;
|
|
18834
|
+
}
|
|
18835
|
+
if (IDENTIFIER_START2.test(char)) {
|
|
18836
|
+
let value = "";
|
|
18837
|
+
while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
|
|
18838
|
+
value += sql[i];
|
|
18839
|
+
i++;
|
|
18840
|
+
}
|
|
18841
|
+
tokens.push({ value, kind: "identifier", quoted: false });
|
|
18842
|
+
continue;
|
|
18843
|
+
}
|
|
18844
|
+
if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
|
|
18845
|
+
tokens.push({ value: char, kind: "punctuation", quoted: false });
|
|
18846
|
+
i++;
|
|
18847
|
+
continue;
|
|
18848
|
+
}
|
|
18849
|
+
i++;
|
|
18850
|
+
}
|
|
18851
|
+
return tokens;
|
|
18852
|
+
}
|
|
18853
|
+
function isKeyword(token, keywords) {
|
|
18854
|
+
if (!token || token.kind !== "identifier" || token.quoted)
|
|
18855
|
+
return false;
|
|
18856
|
+
return keywords.has(token.value.toUpperCase());
|
|
18857
|
+
}
|
|
18858
|
+
function isPunctuation(token, value) {
|
|
18859
|
+
return token?.kind === "punctuation" && token.value === value;
|
|
18860
|
+
}
|
|
18861
|
+
function readQualifiedName(tokens, index) {
|
|
18862
|
+
const first = tokens[index];
|
|
18863
|
+
if (!first || first.kind !== "identifier")
|
|
18864
|
+
return null;
|
|
18865
|
+
const parts = [first.value];
|
|
18866
|
+
let cursor = index + 1;
|
|
18867
|
+
while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
|
|
18868
|
+
parts.push(tokens[cursor + 1].value);
|
|
18869
|
+
cursor += 2;
|
|
18870
|
+
}
|
|
18871
|
+
return { parts, next: cursor };
|
|
18872
|
+
}
|
|
18873
|
+
function decodedVariants(value) {
|
|
18874
|
+
const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
|
|
18875
|
+
return decoded === value ? [] : [decoded];
|
|
18876
|
+
}
|
|
18877
|
+
function fromCodePointOrRaw(hex, whole) {
|
|
18878
|
+
const codePoint = parseInt(hex, 16);
|
|
18879
|
+
return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
|
|
18880
|
+
}
|
|
18881
|
+
function extractTableReferences(sql, options = {}) {
|
|
18882
|
+
const seen = new Set;
|
|
18883
|
+
const references = [];
|
|
18884
|
+
const record = (name) => {
|
|
18885
|
+
const key = name.toLowerCase();
|
|
18886
|
+
if (name.length === 0 || seen.has(key))
|
|
18887
|
+
return;
|
|
18888
|
+
seen.add(key);
|
|
18889
|
+
references.push(name);
|
|
18890
|
+
};
|
|
18891
|
+
const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
|
|
18892
|
+
for (const dialect of dialects) {
|
|
18893
|
+
for (const backslashEscapes of [false, true]) {
|
|
18894
|
+
collectReferences(tokenize(sql, dialect, backslashEscapes), record);
|
|
18895
|
+
}
|
|
18896
|
+
}
|
|
18897
|
+
return references;
|
|
18898
|
+
}
|
|
18899
|
+
function collectReferences(tokens, record) {
|
|
18900
|
+
const recordName = (parts) => {
|
|
18901
|
+
const bare = parts[parts.length - 1];
|
|
18902
|
+
record(bare);
|
|
18903
|
+
for (const variant of decodedVariants(bare))
|
|
18904
|
+
record(variant);
|
|
18905
|
+
if (parts.length > 1)
|
|
18906
|
+
record(parts.join("."));
|
|
18907
|
+
};
|
|
18908
|
+
let i = 0;
|
|
18909
|
+
while (i < tokens.length) {
|
|
18910
|
+
if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
|
|
18911
|
+
i++;
|
|
18912
|
+
continue;
|
|
18913
|
+
}
|
|
18914
|
+
const introducer = tokens[i].value.toUpperCase();
|
|
18915
|
+
const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
|
|
18916
|
+
let cursor = i + 1;
|
|
18917
|
+
while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
|
|
18918
|
+
cursor++;
|
|
18919
|
+
while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
|
|
18920
|
+
cursor++;
|
|
18921
|
+
}
|
|
18922
|
+
let expectTable = true;
|
|
18923
|
+
while (expectTable) {
|
|
18924
|
+
expectTable = false;
|
|
18925
|
+
const name = readQualifiedName(tokens, cursor);
|
|
18926
|
+
if (!name)
|
|
18927
|
+
break;
|
|
18928
|
+
const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
|
|
18929
|
+
if (!isFunctionCall)
|
|
18930
|
+
recordName(name.parts);
|
|
18931
|
+
cursor = name.next;
|
|
18932
|
+
if (isFunctionCall)
|
|
18933
|
+
break;
|
|
18934
|
+
if (isKeyword(tokens[cursor], AS_KEYWORD))
|
|
18935
|
+
cursor++;
|
|
18936
|
+
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
|
|
18937
|
+
cursor++;
|
|
18938
|
+
}
|
|
18939
|
+
while (isPunctuation(tokens[cursor], "(")) {
|
|
18940
|
+
let depth = 0;
|
|
18941
|
+
do {
|
|
18942
|
+
if (isPunctuation(tokens[cursor], "("))
|
|
18943
|
+
depth++;
|
|
18944
|
+
else if (isPunctuation(tokens[cursor], ")"))
|
|
18945
|
+
depth--;
|
|
18946
|
+
cursor++;
|
|
18947
|
+
} while (depth > 0 && cursor < tokens.length);
|
|
18948
|
+
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
|
|
18949
|
+
cursor++;
|
|
18950
|
+
}
|
|
18951
|
+
if (isPunctuation(tokens[cursor], ",")) {
|
|
18952
|
+
cursor++;
|
|
18953
|
+
expectTable = true;
|
|
18954
|
+
}
|
|
18955
|
+
}
|
|
18956
|
+
i = Math.max(cursor, i + 1);
|
|
18957
|
+
}
|
|
18958
|
+
let index = 0;
|
|
18959
|
+
while (index < tokens.length) {
|
|
18960
|
+
const token = tokens[index];
|
|
18961
|
+
if (!token || token.kind !== "identifier") {
|
|
18962
|
+
index++;
|
|
18963
|
+
continue;
|
|
18964
|
+
}
|
|
18965
|
+
const name = readQualifiedName(tokens, index);
|
|
18966
|
+
for (let part = 0;part < name.parts.length; part++) {
|
|
18967
|
+
const value = name.parts[part];
|
|
18968
|
+
const isQuoted = tokens[index + part * 2]?.quoted === true;
|
|
18969
|
+
if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
|
|
18970
|
+
record(value);
|
|
18971
|
+
for (const variant of decodedVariants(value))
|
|
18972
|
+
record(variant);
|
|
18973
|
+
}
|
|
18974
|
+
}
|
|
18975
|
+
if (name.parts.length > 1)
|
|
18976
|
+
record(name.parts.join("."));
|
|
18977
|
+
index = name.next;
|
|
18978
|
+
}
|
|
18979
|
+
}
|
|
18980
|
+
var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
|
|
18981
|
+
var init_sql_tables = __esm(() => {
|
|
18982
|
+
init_sql_lexical();
|
|
18983
|
+
TABLE_INTRODUCERS = new Set([
|
|
18984
|
+
"FROM",
|
|
18985
|
+
"JOIN",
|
|
18986
|
+
"INTO",
|
|
18987
|
+
"UPDATE",
|
|
18988
|
+
"TABLE",
|
|
18989
|
+
"TRUNCATE",
|
|
18990
|
+
"COPY",
|
|
18991
|
+
"USING",
|
|
18992
|
+
"STRAIGHT_JOIN"
|
|
18993
|
+
]);
|
|
18994
|
+
PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
|
|
18995
|
+
SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
|
|
18996
|
+
AS_KEYWORD = new Set(["AS"]);
|
|
18997
|
+
POST_TABLE_KEYWORDS = new Set([
|
|
18998
|
+
"AS",
|
|
18999
|
+
"ON",
|
|
19000
|
+
"USING",
|
|
19001
|
+
"WHERE",
|
|
19002
|
+
"GROUP",
|
|
19003
|
+
"ORDER",
|
|
19004
|
+
"HAVING",
|
|
19005
|
+
"LIMIT",
|
|
19006
|
+
"OFFSET",
|
|
19007
|
+
"FETCH",
|
|
19008
|
+
"WINDOW",
|
|
19009
|
+
"UNION",
|
|
19010
|
+
"INTERSECT",
|
|
19011
|
+
"EXCEPT",
|
|
19012
|
+
"JOIN",
|
|
19013
|
+
"INNER",
|
|
19014
|
+
"LEFT",
|
|
19015
|
+
"RIGHT",
|
|
19016
|
+
"FULL",
|
|
19017
|
+
"OUTER",
|
|
19018
|
+
"CROSS",
|
|
19019
|
+
"NATURAL",
|
|
19020
|
+
"STRAIGHT_JOIN",
|
|
19021
|
+
"SET",
|
|
19022
|
+
"VALUES",
|
|
19023
|
+
"SELECT",
|
|
19024
|
+
"RETURNING",
|
|
19025
|
+
"FOR",
|
|
19026
|
+
"INTO",
|
|
19027
|
+
"PARTITION",
|
|
19028
|
+
"WITH",
|
|
19029
|
+
"TABLESAMPLE",
|
|
19030
|
+
"FORCE",
|
|
19031
|
+
"IGNORE",
|
|
19032
|
+
"USE"
|
|
19033
|
+
]);
|
|
19034
|
+
RESERVED_KEYWORDS = new Set([
|
|
19035
|
+
"ALL",
|
|
19036
|
+
"AND",
|
|
19037
|
+
"AS",
|
|
19038
|
+
"ASC",
|
|
19039
|
+
"CASE",
|
|
19040
|
+
"CROSS",
|
|
19041
|
+
"DESC",
|
|
19042
|
+
"DISTINCT",
|
|
19043
|
+
"ELSE",
|
|
19044
|
+
"FALSE",
|
|
19045
|
+
"FOR",
|
|
19046
|
+
"FROM",
|
|
19047
|
+
"GROUP",
|
|
19048
|
+
"HAVING",
|
|
19049
|
+
"IN",
|
|
19050
|
+
"INNER",
|
|
19051
|
+
"INTO",
|
|
19052
|
+
"IS",
|
|
19053
|
+
"JOIN",
|
|
19054
|
+
"LEFT",
|
|
19055
|
+
"LIKE",
|
|
19056
|
+
"LIMIT",
|
|
19057
|
+
"NOT",
|
|
19058
|
+
"NULL",
|
|
19059
|
+
"ON",
|
|
19060
|
+
"OR",
|
|
19061
|
+
"ORDER",
|
|
19062
|
+
"OUTER",
|
|
19063
|
+
"RIGHT",
|
|
19064
|
+
"SELECT",
|
|
19065
|
+
"THEN",
|
|
19066
|
+
"TRUE",
|
|
19067
|
+
"UNION",
|
|
19068
|
+
"USING",
|
|
19069
|
+
"VALUES",
|
|
19070
|
+
"WHEN",
|
|
19071
|
+
"WHERE",
|
|
19072
|
+
"WITH"
|
|
19073
|
+
]);
|
|
19074
|
+
IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
|
|
19075
|
+
IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
|
|
19076
|
+
ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
|
|
19077
|
+
});
|
|
19078
|
+
|
|
18421
19079
|
// src/core/permission/sql-analysis.ts
|
|
18422
19080
|
function normalizeSQL(sql) {
|
|
18423
19081
|
return sql.replace(/--[^\n]*\n/g, `
|
|
@@ -19010,6 +19668,9 @@ var init_engine_hints = __esm(() => {
|
|
|
19010
19668
|
});
|
|
19011
19669
|
|
|
19012
19670
|
// src/core/audit/integration-helper.ts
|
|
19671
|
+
function sqlDialectFor(engine) {
|
|
19672
|
+
return SQL_AUDIT_DIALECTS.includes(engine) ? engine : null;
|
|
19673
|
+
}
|
|
19013
19674
|
async function getAuditLogger(config, configPath, connectionName) {
|
|
19014
19675
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
19015
19676
|
const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
@@ -19051,6 +19712,8 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
|
|
|
19051
19712
|
const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
|
|
19052
19713
|
const engine = config.connection?.system || "postgresql";
|
|
19053
19714
|
const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
|
|
19715
|
+
const auditDialect = sqlDialectFor(engine);
|
|
19716
|
+
const blacklistChecked = auditDialect ? extractTableReferences(outcome.sql ?? "", { dialect: auditDialect }) : [];
|
|
19054
19717
|
let tier = outcome.sideEffectTier ?? getEngineCapability(engine, commandName).tier;
|
|
19055
19718
|
if (options.dryRun || options.plan) {
|
|
19056
19719
|
tier = "dry-run";
|
|
@@ -19075,6 +19738,7 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
|
|
|
19075
19738
|
...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
|
|
19076
19739
|
metadata: {
|
|
19077
19740
|
...outcome.metadata ?? {},
|
|
19741
|
+
...blacklistChecked.length > 0 && { blacklist_checked: blacklistChecked },
|
|
19078
19742
|
connection_name: connectionName,
|
|
19079
19743
|
environment: config.effectiveEnvironment ?? null
|
|
19080
19744
|
}
|
|
@@ -19087,15 +19751,17 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
|
|
|
19087
19751
|
};
|
|
19088
19752
|
}
|
|
19089
19753
|
}
|
|
19090
|
-
var _sessionIdService = null, _loggers, AuditRequiredError;
|
|
19754
|
+
var SQL_AUDIT_DIALECTS, _sessionIdService = null, _loggers, AuditRequiredError;
|
|
19091
19755
|
var init_integration_helper = __esm(() => {
|
|
19092
19756
|
init_logger2();
|
|
19093
19757
|
init_session_id();
|
|
19094
19758
|
init_config_binding();
|
|
19095
19759
|
init_config();
|
|
19096
19760
|
init_capabilities();
|
|
19761
|
+
init_sql_tables();
|
|
19097
19762
|
init_redaction();
|
|
19098
19763
|
init_engine_hints();
|
|
19764
|
+
SQL_AUDIT_DIALECTS = ["postgresql", "mysql", "mariadb"];
|
|
19099
19765
|
_loggers = new Map;
|
|
19100
19766
|
AuditRequiredError = class AuditRequiredError extends Error {
|
|
19101
19767
|
constructor(detail) {
|
|
@@ -19170,73 +19836,6 @@ async function getAllTablesFromAdapter(adapter) {
|
|
|
19170
19836
|
}
|
|
19171
19837
|
var init_error_suggester = () => {};
|
|
19172
19838
|
|
|
19173
|
-
// src/core/mongo/path-matcher.ts
|
|
19174
|
-
function compilePatterns(raw) {
|
|
19175
|
-
const patterns = [];
|
|
19176
|
-
const rejected = [];
|
|
19177
|
-
for (const entry of raw) {
|
|
19178
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
19179
|
-
rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
|
|
19180
|
-
continue;
|
|
19181
|
-
}
|
|
19182
|
-
const segments = entry.split(".");
|
|
19183
|
-
if (segments.length === 0 || segments.some((s) => s.length === 0)) {
|
|
19184
|
-
rejected.push({ raw: entry, reason: "empty path segment" });
|
|
19185
|
-
continue;
|
|
19186
|
-
}
|
|
19187
|
-
const wildcardIndices = segments.map((s, i) => s.includes("*") ? i : -1).filter((i) => i >= 0);
|
|
19188
|
-
if (wildcardIndices.length === 0) {
|
|
19189
|
-
patterns.push({ raw: entry, segments, wildcardTail: false });
|
|
19190
|
-
continue;
|
|
19191
|
-
}
|
|
19192
|
-
const lastIndex = segments.length - 1;
|
|
19193
|
-
const onlyTail = wildcardIndices.length === 1 && wildcardIndices[0] === lastIndex && segments[lastIndex] === "*";
|
|
19194
|
-
if (!onlyTail) {
|
|
19195
|
-
rejected.push({ raw: entry, reason: "wildcard must be the final segment" });
|
|
19196
|
-
continue;
|
|
19197
|
-
}
|
|
19198
|
-
if (segments.length === 1) {
|
|
19199
|
-
rejected.push({ raw: entry, reason: "wildcard must have a parent path" });
|
|
19200
|
-
continue;
|
|
19201
|
-
}
|
|
19202
|
-
patterns.push({ raw: entry, segments: segments.slice(0, -1), wildcardTail: true });
|
|
19203
|
-
}
|
|
19204
|
-
return { patterns, rejected };
|
|
19205
|
-
}
|
|
19206
|
-
function matchAny(path, patterns) {
|
|
19207
|
-
if (patterns.length === 0)
|
|
19208
|
-
return false;
|
|
19209
|
-
const pathSegments = path.split(".");
|
|
19210
|
-
for (const pat of patterns) {
|
|
19211
|
-
if (pat.wildcardTail) {
|
|
19212
|
-
if (pathSegments.length < pat.segments.length)
|
|
19213
|
-
continue;
|
|
19214
|
-
let ok = true;
|
|
19215
|
-
for (let i = 0;i < pat.segments.length; i++) {
|
|
19216
|
-
if (pat.segments[i] !== pathSegments[i]) {
|
|
19217
|
-
ok = false;
|
|
19218
|
-
break;
|
|
19219
|
-
}
|
|
19220
|
-
}
|
|
19221
|
-
if (ok)
|
|
19222
|
-
return true;
|
|
19223
|
-
} else {
|
|
19224
|
-
if (pat.segments.length !== pathSegments.length)
|
|
19225
|
-
continue;
|
|
19226
|
-
let ok = true;
|
|
19227
|
-
for (let i = 0;i < pat.segments.length; i++) {
|
|
19228
|
-
if (pat.segments[i] !== pathSegments[i]) {
|
|
19229
|
-
ok = false;
|
|
19230
|
-
break;
|
|
19231
|
-
}
|
|
19232
|
-
}
|
|
19233
|
-
if (ok)
|
|
19234
|
-
return true;
|
|
19235
|
-
}
|
|
19236
|
-
}
|
|
19237
|
-
return false;
|
|
19238
|
-
}
|
|
19239
|
-
|
|
19240
19839
|
// src/utils/bounded-parallel.ts
|
|
19241
19840
|
async function mapWithConcurrency(items, limit, worker) {
|
|
19242
19841
|
if (items.length === 0)
|
|
@@ -21322,7 +21921,11 @@ function markRedactedColumns(cols, collection, blacklist) {
|
|
|
21322
21921
|
const raw = (blacklist.columns ?? {})[collection];
|
|
21323
21922
|
if (!raw || raw.length === 0)
|
|
21324
21923
|
return cols;
|
|
21325
|
-
const { patterns } = compilePatterns(raw);
|
|
21924
|
+
const { patterns, rejected } = compilePatterns(raw);
|
|
21925
|
+
if (rejected.length > 0) {
|
|
21926
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
21927
|
+
throw new Error(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`);
|
|
21928
|
+
}
|
|
21326
21929
|
if (patterns.length === 0)
|
|
21327
21930
|
return cols;
|
|
21328
21931
|
return cols.map((c) => matchAny(c.name, patterns) ? { ...c, redacted: true } : c);
|
|
@@ -21770,6 +22373,7 @@ var init_schema = __esm(() => {
|
|
|
21770
22373
|
init_validation();
|
|
21771
22374
|
init_connection_selector();
|
|
21772
22375
|
init_error_suggester();
|
|
22376
|
+
init_path_matcher();
|
|
21773
22377
|
init_config_path();
|
|
21774
22378
|
ALLOWED_FORMATS2 = ["table", "json"];
|
|
21775
22379
|
schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 100, max 1000). Ignored on SQL connections.").option("--sample-method <method>", 'MongoDB only: "random" (default, uses $sample) or "natural" (uses find().limit()). Ignored on SQL connections.', "random").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
|
|
@@ -21874,355 +22478,6 @@ async function openInBrowser(target) {
|
|
|
21874
22478
|
}
|
|
21875
22479
|
var init_opener = () => {};
|
|
21876
22480
|
|
|
21877
|
-
// src/utils/sql-tables.ts
|
|
21878
|
-
var exports_sql_tables = {};
|
|
21879
|
-
__export(exports_sql_tables, {
|
|
21880
|
-
extractTableReferences: () => extractTableReferences
|
|
21881
|
-
});
|
|
21882
|
-
function tokenize(sql, dialect, backslashEscapes) {
|
|
21883
|
-
const tokens = [];
|
|
21884
|
-
const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
|
|
21885
|
-
let i = 0;
|
|
21886
|
-
while (i < sql.length) {
|
|
21887
|
-
const char = sql[i];
|
|
21888
|
-
const dashFollowerCode = sql.charCodeAt(i + 2);
|
|
21889
|
-
if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
|
|
21890
|
-
while (i < sql.length && sql[i] !== `
|
|
21891
|
-
`)
|
|
21892
|
-
i++;
|
|
21893
|
-
continue;
|
|
21894
|
-
}
|
|
21895
|
-
if (mysqlDialect && char === "#") {
|
|
21896
|
-
while (i < sql.length && sql[i] !== `
|
|
21897
|
-
`)
|
|
21898
|
-
i++;
|
|
21899
|
-
continue;
|
|
21900
|
-
}
|
|
21901
|
-
if (char === "/" && sql[i + 1] === "*") {
|
|
21902
|
-
if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
|
|
21903
|
-
const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
|
|
21904
|
-
const closingIndex = sql.indexOf("*/", i + prefixLength);
|
|
21905
|
-
const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
|
|
21906
|
-
const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
|
|
21907
|
-
tokens.push(...tokenize(body, dialect, backslashEscapes));
|
|
21908
|
-
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
21909
|
-
continue;
|
|
21910
|
-
}
|
|
21911
|
-
const nests = dialect === "postgresql";
|
|
21912
|
-
let depth = 1;
|
|
21913
|
-
i += 2;
|
|
21914
|
-
while (i < sql.length && depth > 0) {
|
|
21915
|
-
if (nests && sql[i] === "/" && sql[i + 1] === "*") {
|
|
21916
|
-
depth++;
|
|
21917
|
-
i += 2;
|
|
21918
|
-
continue;
|
|
21919
|
-
}
|
|
21920
|
-
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
21921
|
-
depth--;
|
|
21922
|
-
i += 2;
|
|
21923
|
-
continue;
|
|
21924
|
-
}
|
|
21925
|
-
i++;
|
|
21926
|
-
}
|
|
21927
|
-
continue;
|
|
21928
|
-
}
|
|
21929
|
-
if (dialect === "postgresql" && char === "$") {
|
|
21930
|
-
const delimiter = dollarQuoteDelimiterAt(sql, i);
|
|
21931
|
-
if (delimiter) {
|
|
21932
|
-
i += delimiter.length;
|
|
21933
|
-
const closingIndex = sql.indexOf(delimiter, i);
|
|
21934
|
-
i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
|
|
21935
|
-
continue;
|
|
21936
|
-
}
|
|
21937
|
-
}
|
|
21938
|
-
if (char === "'") {
|
|
21939
|
-
i++;
|
|
21940
|
-
while (i < sql.length) {
|
|
21941
|
-
if (sql[i] === "'") {
|
|
21942
|
-
if (sql[i + 1] === "'") {
|
|
21943
|
-
i += 2;
|
|
21944
|
-
continue;
|
|
21945
|
-
}
|
|
21946
|
-
i++;
|
|
21947
|
-
break;
|
|
21948
|
-
}
|
|
21949
|
-
if (backslashEscapes && sql[i] === "\\") {
|
|
21950
|
-
i += 2;
|
|
21951
|
-
continue;
|
|
21952
|
-
}
|
|
21953
|
-
i++;
|
|
21954
|
-
}
|
|
21955
|
-
continue;
|
|
21956
|
-
}
|
|
21957
|
-
if (char === '"' || char === "`") {
|
|
21958
|
-
const quote = char;
|
|
21959
|
-
i++;
|
|
21960
|
-
let value = "";
|
|
21961
|
-
while (i < sql.length) {
|
|
21962
|
-
if (sql[i] === quote) {
|
|
21963
|
-
if (sql[i + 1] === quote) {
|
|
21964
|
-
value += quote;
|
|
21965
|
-
i += 2;
|
|
21966
|
-
continue;
|
|
21967
|
-
}
|
|
21968
|
-
i++;
|
|
21969
|
-
break;
|
|
21970
|
-
}
|
|
21971
|
-
if (backslashEscapes && sql[i] === "\\") {
|
|
21972
|
-
value += sql[i + 1] ?? "";
|
|
21973
|
-
i += 2;
|
|
21974
|
-
continue;
|
|
21975
|
-
}
|
|
21976
|
-
value += sql[i];
|
|
21977
|
-
i++;
|
|
21978
|
-
}
|
|
21979
|
-
tokens.push({ value, kind: "identifier", quoted: true });
|
|
21980
|
-
continue;
|
|
21981
|
-
}
|
|
21982
|
-
if (IDENTIFIER_START2.test(char)) {
|
|
21983
|
-
let value = "";
|
|
21984
|
-
while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
|
|
21985
|
-
value += sql[i];
|
|
21986
|
-
i++;
|
|
21987
|
-
}
|
|
21988
|
-
tokens.push({ value, kind: "identifier", quoted: false });
|
|
21989
|
-
continue;
|
|
21990
|
-
}
|
|
21991
|
-
if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
|
|
21992
|
-
tokens.push({ value: char, kind: "punctuation", quoted: false });
|
|
21993
|
-
i++;
|
|
21994
|
-
continue;
|
|
21995
|
-
}
|
|
21996
|
-
i++;
|
|
21997
|
-
}
|
|
21998
|
-
return tokens;
|
|
21999
|
-
}
|
|
22000
|
-
function isKeyword(token, keywords) {
|
|
22001
|
-
if (!token || token.kind !== "identifier" || token.quoted)
|
|
22002
|
-
return false;
|
|
22003
|
-
return keywords.has(token.value.toUpperCase());
|
|
22004
|
-
}
|
|
22005
|
-
function isPunctuation(token, value) {
|
|
22006
|
-
return token?.kind === "punctuation" && token.value === value;
|
|
22007
|
-
}
|
|
22008
|
-
function readQualifiedName(tokens, index) {
|
|
22009
|
-
const first = tokens[index];
|
|
22010
|
-
if (!first || first.kind !== "identifier")
|
|
22011
|
-
return null;
|
|
22012
|
-
const parts = [first.value];
|
|
22013
|
-
let cursor = index + 1;
|
|
22014
|
-
while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
|
|
22015
|
-
parts.push(tokens[cursor + 1].value);
|
|
22016
|
-
cursor += 2;
|
|
22017
|
-
}
|
|
22018
|
-
return { parts, next: cursor };
|
|
22019
|
-
}
|
|
22020
|
-
function decodedVariants(value) {
|
|
22021
|
-
const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
|
|
22022
|
-
return decoded === value ? [] : [decoded];
|
|
22023
|
-
}
|
|
22024
|
-
function fromCodePointOrRaw(hex, whole) {
|
|
22025
|
-
const codePoint = parseInt(hex, 16);
|
|
22026
|
-
return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
|
|
22027
|
-
}
|
|
22028
|
-
function extractTableReferences(sql, options = {}) {
|
|
22029
|
-
const seen = new Set;
|
|
22030
|
-
const references = [];
|
|
22031
|
-
const record = (name) => {
|
|
22032
|
-
const key = name.toLowerCase();
|
|
22033
|
-
if (name.length === 0 || seen.has(key))
|
|
22034
|
-
return;
|
|
22035
|
-
seen.add(key);
|
|
22036
|
-
references.push(name);
|
|
22037
|
-
};
|
|
22038
|
-
const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
|
|
22039
|
-
for (const dialect of dialects) {
|
|
22040
|
-
for (const backslashEscapes of [false, true]) {
|
|
22041
|
-
collectReferences(tokenize(sql, dialect, backslashEscapes), record);
|
|
22042
|
-
}
|
|
22043
|
-
}
|
|
22044
|
-
return references;
|
|
22045
|
-
}
|
|
22046
|
-
function collectReferences(tokens, record) {
|
|
22047
|
-
const recordName = (parts) => {
|
|
22048
|
-
const bare = parts[parts.length - 1];
|
|
22049
|
-
record(bare);
|
|
22050
|
-
for (const variant of decodedVariants(bare))
|
|
22051
|
-
record(variant);
|
|
22052
|
-
if (parts.length > 1)
|
|
22053
|
-
record(parts.join("."));
|
|
22054
|
-
};
|
|
22055
|
-
let i = 0;
|
|
22056
|
-
while (i < tokens.length) {
|
|
22057
|
-
if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
|
|
22058
|
-
i++;
|
|
22059
|
-
continue;
|
|
22060
|
-
}
|
|
22061
|
-
const introducer = tokens[i].value.toUpperCase();
|
|
22062
|
-
const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
|
|
22063
|
-
let cursor = i + 1;
|
|
22064
|
-
while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
|
|
22065
|
-
cursor++;
|
|
22066
|
-
while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
|
|
22067
|
-
cursor++;
|
|
22068
|
-
}
|
|
22069
|
-
let expectTable = true;
|
|
22070
|
-
while (expectTable) {
|
|
22071
|
-
expectTable = false;
|
|
22072
|
-
const name = readQualifiedName(tokens, cursor);
|
|
22073
|
-
if (!name)
|
|
22074
|
-
break;
|
|
22075
|
-
const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
|
|
22076
|
-
if (!isFunctionCall)
|
|
22077
|
-
recordName(name.parts);
|
|
22078
|
-
cursor = name.next;
|
|
22079
|
-
if (isFunctionCall)
|
|
22080
|
-
break;
|
|
22081
|
-
if (isKeyword(tokens[cursor], AS_KEYWORD))
|
|
22082
|
-
cursor++;
|
|
22083
|
-
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
|
|
22084
|
-
cursor++;
|
|
22085
|
-
}
|
|
22086
|
-
while (isPunctuation(tokens[cursor], "(")) {
|
|
22087
|
-
let depth = 0;
|
|
22088
|
-
do {
|
|
22089
|
-
if (isPunctuation(tokens[cursor], "("))
|
|
22090
|
-
depth++;
|
|
22091
|
-
else if (isPunctuation(tokens[cursor], ")"))
|
|
22092
|
-
depth--;
|
|
22093
|
-
cursor++;
|
|
22094
|
-
} while (depth > 0 && cursor < tokens.length);
|
|
22095
|
-
if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
|
|
22096
|
-
cursor++;
|
|
22097
|
-
}
|
|
22098
|
-
if (isPunctuation(tokens[cursor], ",")) {
|
|
22099
|
-
cursor++;
|
|
22100
|
-
expectTable = true;
|
|
22101
|
-
}
|
|
22102
|
-
}
|
|
22103
|
-
i = Math.max(cursor, i + 1);
|
|
22104
|
-
}
|
|
22105
|
-
let index = 0;
|
|
22106
|
-
while (index < tokens.length) {
|
|
22107
|
-
const token = tokens[index];
|
|
22108
|
-
if (!token || token.kind !== "identifier") {
|
|
22109
|
-
index++;
|
|
22110
|
-
continue;
|
|
22111
|
-
}
|
|
22112
|
-
const name = readQualifiedName(tokens, index);
|
|
22113
|
-
for (let part = 0;part < name.parts.length; part++) {
|
|
22114
|
-
const value = name.parts[part];
|
|
22115
|
-
const isQuoted = tokens[index + part * 2]?.quoted === true;
|
|
22116
|
-
if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
|
|
22117
|
-
record(value);
|
|
22118
|
-
for (const variant of decodedVariants(value))
|
|
22119
|
-
record(variant);
|
|
22120
|
-
}
|
|
22121
|
-
}
|
|
22122
|
-
if (name.parts.length > 1)
|
|
22123
|
-
record(name.parts.join("."));
|
|
22124
|
-
index = name.next;
|
|
22125
|
-
}
|
|
22126
|
-
}
|
|
22127
|
-
var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
|
|
22128
|
-
var init_sql_tables = __esm(() => {
|
|
22129
|
-
init_sql_lexical();
|
|
22130
|
-
TABLE_INTRODUCERS = new Set([
|
|
22131
|
-
"FROM",
|
|
22132
|
-
"JOIN",
|
|
22133
|
-
"INTO",
|
|
22134
|
-
"UPDATE",
|
|
22135
|
-
"TABLE",
|
|
22136
|
-
"TRUNCATE",
|
|
22137
|
-
"COPY",
|
|
22138
|
-
"USING",
|
|
22139
|
-
"STRAIGHT_JOIN"
|
|
22140
|
-
]);
|
|
22141
|
-
PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
|
|
22142
|
-
SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
|
|
22143
|
-
AS_KEYWORD = new Set(["AS"]);
|
|
22144
|
-
POST_TABLE_KEYWORDS = new Set([
|
|
22145
|
-
"AS",
|
|
22146
|
-
"ON",
|
|
22147
|
-
"USING",
|
|
22148
|
-
"WHERE",
|
|
22149
|
-
"GROUP",
|
|
22150
|
-
"ORDER",
|
|
22151
|
-
"HAVING",
|
|
22152
|
-
"LIMIT",
|
|
22153
|
-
"OFFSET",
|
|
22154
|
-
"FETCH",
|
|
22155
|
-
"WINDOW",
|
|
22156
|
-
"UNION",
|
|
22157
|
-
"INTERSECT",
|
|
22158
|
-
"EXCEPT",
|
|
22159
|
-
"JOIN",
|
|
22160
|
-
"INNER",
|
|
22161
|
-
"LEFT",
|
|
22162
|
-
"RIGHT",
|
|
22163
|
-
"FULL",
|
|
22164
|
-
"OUTER",
|
|
22165
|
-
"CROSS",
|
|
22166
|
-
"NATURAL",
|
|
22167
|
-
"STRAIGHT_JOIN",
|
|
22168
|
-
"SET",
|
|
22169
|
-
"VALUES",
|
|
22170
|
-
"SELECT",
|
|
22171
|
-
"RETURNING",
|
|
22172
|
-
"FOR",
|
|
22173
|
-
"INTO",
|
|
22174
|
-
"PARTITION",
|
|
22175
|
-
"WITH",
|
|
22176
|
-
"TABLESAMPLE",
|
|
22177
|
-
"FORCE",
|
|
22178
|
-
"IGNORE",
|
|
22179
|
-
"USE"
|
|
22180
|
-
]);
|
|
22181
|
-
RESERVED_KEYWORDS = new Set([
|
|
22182
|
-
"ALL",
|
|
22183
|
-
"AND",
|
|
22184
|
-
"AS",
|
|
22185
|
-
"ASC",
|
|
22186
|
-
"CASE",
|
|
22187
|
-
"CROSS",
|
|
22188
|
-
"DESC",
|
|
22189
|
-
"DISTINCT",
|
|
22190
|
-
"ELSE",
|
|
22191
|
-
"FALSE",
|
|
22192
|
-
"FOR",
|
|
22193
|
-
"FROM",
|
|
22194
|
-
"GROUP",
|
|
22195
|
-
"HAVING",
|
|
22196
|
-
"IN",
|
|
22197
|
-
"INNER",
|
|
22198
|
-
"INTO",
|
|
22199
|
-
"IS",
|
|
22200
|
-
"JOIN",
|
|
22201
|
-
"LEFT",
|
|
22202
|
-
"LIKE",
|
|
22203
|
-
"LIMIT",
|
|
22204
|
-
"NOT",
|
|
22205
|
-
"NULL",
|
|
22206
|
-
"ON",
|
|
22207
|
-
"OR",
|
|
22208
|
-
"ORDER",
|
|
22209
|
-
"OUTER",
|
|
22210
|
-
"RIGHT",
|
|
22211
|
-
"SELECT",
|
|
22212
|
-
"THEN",
|
|
22213
|
-
"TRUE",
|
|
22214
|
-
"UNION",
|
|
22215
|
-
"USING",
|
|
22216
|
-
"VALUES",
|
|
22217
|
-
"WHEN",
|
|
22218
|
-
"WHERE",
|
|
22219
|
-
"WITH"
|
|
22220
|
-
]);
|
|
22221
|
-
IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
|
|
22222
|
-
IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
|
|
22223
|
-
ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
|
|
22224
|
-
});
|
|
22225
|
-
|
|
22226
22481
|
// src/core/limits.ts
|
|
22227
22482
|
var DEFAULT_QUERY_ONLY_LIMIT = 1000;
|
|
22228
22483
|
|
|
@@ -22402,7 +22657,10 @@ function maskMongoRowsForCollections(rows, collections, blacklist) {
|
|
|
22402
22657
|
const prefixKey = `\x00${scope.collection}@${scope.prefix}`;
|
|
22403
22658
|
return maskMongoRows(atTopLevel, prefixKey, {
|
|
22404
22659
|
...blacklist,
|
|
22405
|
-
columns: {
|
|
22660
|
+
columns: {
|
|
22661
|
+
...columns,
|
|
22662
|
+
[prefixKey]: rules.map((rule) => `${escapeGlob(scope.prefix)}.${rule}`)
|
|
22663
|
+
}
|
|
22406
22664
|
});
|
|
22407
22665
|
}, rows);
|
|
22408
22666
|
}
|
|
@@ -22411,10 +22669,14 @@ function maskMongoRows(rows, collection, blacklist) {
|
|
|
22411
22669
|
const raw = columns[collection] ?? findCaseInsensitive(columns, collection);
|
|
22412
22670
|
if (!raw || raw.length === 0)
|
|
22413
22671
|
return rows;
|
|
22414
|
-
const { patterns } = compilePatterns(raw);
|
|
22672
|
+
const { patterns, rejected } = compilePatterns(raw);
|
|
22673
|
+
if (rejected.length > 0) {
|
|
22674
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
22675
|
+
throw new BlacklistError(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`, collection, "READ");
|
|
22676
|
+
}
|
|
22415
22677
|
if (patterns.length === 0)
|
|
22416
22678
|
return rows;
|
|
22417
|
-
const idAffected = patterns.some((p) => p.segments.length === 1 && p.segments[0]
|
|
22679
|
+
const idAffected = patterns.some((p) => p.segments.length === 1 && !p.wildcardTail && globMatches(p.segments[0], "_id"));
|
|
22418
22680
|
if (idAffected) {
|
|
22419
22681
|
console.error(`[blacklist] collection '${collection}' blacklists '_id'; read paths still expose _id to preserve document references.`);
|
|
22420
22682
|
}
|
|
@@ -22456,7 +22718,12 @@ function findCaseInsensitive(columns, name) {
|
|
|
22456
22718
|
return;
|
|
22457
22719
|
}
|
|
22458
22720
|
var REDACTED2 = "[REDACTED]";
|
|
22459
|
-
var init_field_masker = () => {
|
|
22721
|
+
var init_field_masker = __esm(() => {
|
|
22722
|
+
init_blacklist();
|
|
22723
|
+
init_path_matcher();
|
|
22724
|
+
init_glob();
|
|
22725
|
+
init_glob();
|
|
22726
|
+
});
|
|
22460
22727
|
|
|
22461
22728
|
// src/core/mongo/collection-references.ts
|
|
22462
22729
|
function recordTarget(value, found) {
|
|
@@ -24224,6 +24491,7 @@ var init_query = __esm(() => {
|
|
|
24224
24491
|
init_opener();
|
|
24225
24492
|
init_query_executor();
|
|
24226
24493
|
init_config();
|
|
24494
|
+
init_blacklist_manager();
|
|
24227
24495
|
init_blacklist_validator();
|
|
24228
24496
|
init_config_path();
|
|
24229
24497
|
init_validation();
|
|
@@ -26161,6 +26429,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
|
|
|
26161
26429
|
}
|
|
26162
26430
|
var init_q_mongo = __esm(() => {
|
|
26163
26431
|
init_adapters();
|
|
26432
|
+
init_blacklist_manager();
|
|
26164
26433
|
init_blacklist_validator();
|
|
26165
26434
|
init_field_masker();
|
|
26166
26435
|
init_collection_references();
|
|
@@ -26524,6 +26793,7 @@ var init_q = __esm(() => {
|
|
|
26524
26793
|
init_adapters();
|
|
26525
26794
|
init_config();
|
|
26526
26795
|
init_config_path();
|
|
26796
|
+
init_blacklist_manager();
|
|
26527
26797
|
init_blacklist_validator();
|
|
26528
26798
|
init_blacklist();
|
|
26529
26799
|
init_permission_guard();
|
|
@@ -26792,6 +27062,365 @@ function previewDelete(collection, filter) {
|
|
|
26792
27062
|
}
|
|
26793
27063
|
var INDENT = 2;
|
|
26794
27064
|
|
|
27065
|
+
// src/utils/where-parser.ts
|
|
27066
|
+
function parseWhereClause(whereClause) {
|
|
27067
|
+
if (!whereClause || whereClause.trim() === "") {
|
|
27068
|
+
throw new Error("WHERE clause cannot be empty");
|
|
27069
|
+
}
|
|
27070
|
+
const conditions = {};
|
|
27071
|
+
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
27072
|
+
for (const part of andParts) {
|
|
27073
|
+
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
27074
|
+
if (!match) {
|
|
27075
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
27076
|
+
}
|
|
27077
|
+
const column = match[1];
|
|
27078
|
+
const valueStr = match[2];
|
|
27079
|
+
if (valueStr === undefined || column === undefined) {
|
|
27080
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
27081
|
+
}
|
|
27082
|
+
const trimmed = valueStr.trim();
|
|
27083
|
+
const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
|
|
27084
|
+
if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
|
|
27085
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
27086
|
+
}
|
|
27087
|
+
const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
|
|
27088
|
+
let value = stripped;
|
|
27089
|
+
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
27090
|
+
value = Number(stripped);
|
|
27091
|
+
}
|
|
27092
|
+
if (stripped === "true")
|
|
27093
|
+
value = true;
|
|
27094
|
+
else if (stripped === "false")
|
|
27095
|
+
value = false;
|
|
27096
|
+
else if (stripped === "null")
|
|
27097
|
+
value = null;
|
|
27098
|
+
conditions[column] = value;
|
|
27099
|
+
}
|
|
27100
|
+
return conditions;
|
|
27101
|
+
}
|
|
27102
|
+
|
|
27103
|
+
// src/core/mongo/dml-plan.ts
|
|
27104
|
+
function permissionSql(operation) {
|
|
27105
|
+
if (operation === "insert")
|
|
27106
|
+
return "INSERT INTO dummy";
|
|
27107
|
+
if (operation === "update")
|
|
27108
|
+
return "UPDATE dummy";
|
|
27109
|
+
return "DELETE FROM dummy";
|
|
27110
|
+
}
|
|
27111
|
+
function broadOperation(operation) {
|
|
27112
|
+
if (operation === "insert")
|
|
27113
|
+
return "INSERT";
|
|
27114
|
+
if (operation === "update")
|
|
27115
|
+
return "UPDATE";
|
|
27116
|
+
return "DELETE";
|
|
27117
|
+
}
|
|
27118
|
+
function pushFactor2(factors, code, severity, message) {
|
|
27119
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
27120
|
+
return;
|
|
27121
|
+
factors.push({ code, severity, message });
|
|
27122
|
+
}
|
|
27123
|
+
function decide2(factors) {
|
|
27124
|
+
if (factors.some((f) => f.severity === "block"))
|
|
27125
|
+
return "BLOCK";
|
|
27126
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
27127
|
+
return "WARN";
|
|
27128
|
+
return "ALLOW";
|
|
27129
|
+
}
|
|
27130
|
+
function classifyMongoUpdate(setDoc) {
|
|
27131
|
+
const fields = new Set;
|
|
27132
|
+
const tierFactors = [];
|
|
27133
|
+
let hasBlock = false;
|
|
27134
|
+
const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
|
|
27135
|
+
if (!hasAnyOperator) {
|
|
27136
|
+
for (const k of Object.keys(setDoc))
|
|
27137
|
+
fields.add(k);
|
|
27138
|
+
return { fields: Array.from(fields), tierFactors, hasBlock };
|
|
27139
|
+
}
|
|
27140
|
+
const seenTiers = new Map;
|
|
27141
|
+
for (const [op, payload] of Object.entries(setDoc)) {
|
|
27142
|
+
if (!op.startsWith("$")) {
|
|
27143
|
+
fields.add(op);
|
|
27144
|
+
continue;
|
|
27145
|
+
}
|
|
27146
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
27147
|
+
for (const k of Object.keys(payload))
|
|
27148
|
+
fields.add(k);
|
|
27149
|
+
}
|
|
27150
|
+
const tier = MONGO_OPERATOR_TIER[op];
|
|
27151
|
+
if (tier === undefined) {
|
|
27152
|
+
hasBlock = true;
|
|
27153
|
+
tierFactors.push({
|
|
27154
|
+
code: "mongo_unknown_operator",
|
|
27155
|
+
severity: "block",
|
|
27156
|
+
message: `Update uses unknown operator '${op}'. Reject by default; add to tier table if intentional.`
|
|
27157
|
+
});
|
|
27158
|
+
continue;
|
|
27159
|
+
}
|
|
27160
|
+
if (tier === "BLOCK") {
|
|
27161
|
+
hasBlock = true;
|
|
27162
|
+
tierFactors.push({
|
|
27163
|
+
code: "mongo_unknown_operator",
|
|
27164
|
+
severity: "block",
|
|
27165
|
+
message: `Update uses '${op}' which executes server-side code. Operation rejected.`
|
|
27166
|
+
});
|
|
27167
|
+
continue;
|
|
27168
|
+
}
|
|
27169
|
+
if (tier === "SAFE")
|
|
27170
|
+
continue;
|
|
27171
|
+
const bucket = seenTiers.get(tier) ?? [];
|
|
27172
|
+
bucket.push(op);
|
|
27173
|
+
seenTiers.set(tier, bucket);
|
|
27174
|
+
}
|
|
27175
|
+
for (const [tier, ops] of seenTiers) {
|
|
27176
|
+
if (tier === "RENAME") {
|
|
27177
|
+
tierFactors.push({
|
|
27178
|
+
code: "mongo_rename_operator",
|
|
27179
|
+
severity: "warn",
|
|
27180
|
+
message: `Update uses ${ops.join(", ")}; a renamed field keeps its value under a name the read mask does not know.`
|
|
27181
|
+
});
|
|
27182
|
+
} else if (tier === "ARITHMETIC") {
|
|
27183
|
+
tierFactors.push({
|
|
27184
|
+
code: "mongo_arithmetic_operator",
|
|
27185
|
+
severity: "warn",
|
|
27186
|
+
message: `Update uses ${ops.join(", ")}; numeric mutation may compound silently.`
|
|
27187
|
+
});
|
|
27188
|
+
} else if (tier === "ARRAY") {
|
|
27189
|
+
tierFactors.push({
|
|
27190
|
+
code: "mongo_array_operator",
|
|
27191
|
+
severity: "warn",
|
|
27192
|
+
message: `Update uses ${ops.join(", ")}; array mutation can grow unboundedly without a size guard.`
|
|
27193
|
+
});
|
|
27194
|
+
} else if (tier === "BITWISE") {
|
|
27195
|
+
tierFactors.push({
|
|
27196
|
+
code: "mongo_bitwise_operator",
|
|
27197
|
+
severity: "warn",
|
|
27198
|
+
message: `Update uses ${ops.join(", ")}; bitwise updates skip type promotion checks.`
|
|
27199
|
+
});
|
|
27200
|
+
}
|
|
27201
|
+
}
|
|
27202
|
+
return { fields: Array.from(fields), tierFactors, hasBlock };
|
|
27203
|
+
}
|
|
27204
|
+
function hasIdEquality(where) {
|
|
27205
|
+
if (!("_id" in where))
|
|
27206
|
+
return false;
|
|
27207
|
+
const value = where._id;
|
|
27208
|
+
if (value === null || value === undefined)
|
|
27209
|
+
return false;
|
|
27210
|
+
if (typeof value === "object")
|
|
27211
|
+
return false;
|
|
27212
|
+
return true;
|
|
27213
|
+
}
|
|
27214
|
+
function hasNonIdEquality(where) {
|
|
27215
|
+
for (const [key, value] of Object.entries(where)) {
|
|
27216
|
+
if (key === "_id")
|
|
27217
|
+
continue;
|
|
27218
|
+
if (key.startsWith("$"))
|
|
27219
|
+
continue;
|
|
27220
|
+
if (value === null)
|
|
27221
|
+
continue;
|
|
27222
|
+
if (typeof value === "object")
|
|
27223
|
+
continue;
|
|
27224
|
+
return true;
|
|
27225
|
+
}
|
|
27226
|
+
return false;
|
|
27227
|
+
}
|
|
27228
|
+
function hasBroadFilter(where) {
|
|
27229
|
+
for (const [key, value] of Object.entries(where)) {
|
|
27230
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(key))
|
|
27231
|
+
return true;
|
|
27232
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
27233
|
+
for (const inner of Object.keys(value)) {
|
|
27234
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
|
|
27235
|
+
return true;
|
|
27236
|
+
}
|
|
27237
|
+
}
|
|
27238
|
+
}
|
|
27239
|
+
return false;
|
|
27240
|
+
}
|
|
27241
|
+
function applyPermission(operation, context, factors) {
|
|
27242
|
+
const permResult = checkPermission(permissionSql(operation), context.permission);
|
|
27243
|
+
if (!permResult.allowed) {
|
|
27244
|
+
pushFactor2(factors, "permission_denied", "block", permResult.reason);
|
|
27245
|
+
}
|
|
27246
|
+
}
|
|
27247
|
+
function applyTableBlacklist(target, context, factors) {
|
|
27248
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
27249
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
27250
|
+
pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
|
|
27251
|
+
}
|
|
27252
|
+
}
|
|
27253
|
+
function applyColumnBlacklist(target, fields, context, factors) {
|
|
27254
|
+
const columns = context.blacklist.columns ?? {};
|
|
27255
|
+
const lower = target.toLowerCase();
|
|
27256
|
+
let raw = [];
|
|
27257
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
27258
|
+
if (t2.toLowerCase() === lower) {
|
|
27259
|
+
raw = cols;
|
|
27260
|
+
break;
|
|
27261
|
+
}
|
|
27262
|
+
}
|
|
27263
|
+
if (raw.length === 0)
|
|
27264
|
+
return;
|
|
27265
|
+
const { patterns, rejected } = compilePatterns(raw);
|
|
27266
|
+
if (rejected.length > 0) {
|
|
27267
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
27268
|
+
pushFactor2(factors, "blacklist_unreadable", "block", `blacklist.columns for '${target}' has entries this matcher cannot read: ${detail}`);
|
|
27269
|
+
return;
|
|
27270
|
+
}
|
|
27271
|
+
if (patterns.length === 0)
|
|
27272
|
+
return;
|
|
27273
|
+
for (const field of fields) {
|
|
27274
|
+
if (matchAny(field, patterns)) {
|
|
27275
|
+
pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted path ${target}.${field}.`);
|
|
27276
|
+
}
|
|
27277
|
+
}
|
|
27278
|
+
}
|
|
27279
|
+
function applySchemaCoverage(target, context, factors) {
|
|
27280
|
+
const schemaTables = Object.keys(context.schema);
|
|
27281
|
+
if (schemaTables.length === 0) {
|
|
27282
|
+
pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
|
|
27283
|
+
return;
|
|
27284
|
+
}
|
|
27285
|
+
const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
27286
|
+
if (!known) {
|
|
27287
|
+
pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
|
|
27288
|
+
}
|
|
27289
|
+
}
|
|
27290
|
+
function buildRecommendations2(factors) {
|
|
27291
|
+
const out = new Set;
|
|
27292
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
27293
|
+
if (codes.has("nonsql_filter_empty")) {
|
|
27294
|
+
out.add("Add an _id equality filter before executing this MongoDB write.");
|
|
27295
|
+
}
|
|
27296
|
+
if (codes.has("nonsql_missing_id")) {
|
|
27297
|
+
out.add("Prefer an _id equality filter before executing this MongoDB write.");
|
|
27298
|
+
}
|
|
27299
|
+
if (codes.has("nonsql_filter_broad")) {
|
|
27300
|
+
out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
|
|
27301
|
+
}
|
|
27302
|
+
if (codes.has("nonsql_unsupported_operator")) {
|
|
27303
|
+
out.add("Restrict the update document to $set / $unset for planner-safe writes.");
|
|
27304
|
+
}
|
|
27305
|
+
if (codes.has("permission_denied")) {
|
|
27306
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
27307
|
+
}
|
|
27308
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
27309
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
27310
|
+
}
|
|
27311
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
27312
|
+
out.add("Refresh schema cache for the target collection before executing.");
|
|
27313
|
+
}
|
|
27314
|
+
out.add("Use --dry-run on the actual write command.");
|
|
27315
|
+
return Array.from(out);
|
|
27316
|
+
}
|
|
27317
|
+
function buildSuggestedCommands2(target, factors) {
|
|
27318
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
27319
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
27320
|
+
return [`dbcli schema ${target} --format json`];
|
|
27321
|
+
}
|
|
27322
|
+
return [];
|
|
27323
|
+
}
|
|
27324
|
+
function flattenInsertPaths(data, prefix = "") {
|
|
27325
|
+
const out = [];
|
|
27326
|
+
for (const [k, v] of Object.entries(data)) {
|
|
27327
|
+
const path2 = prefix === "" ? k : `${prefix}.${k}`;
|
|
27328
|
+
out.push(path2);
|
|
27329
|
+
if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
|
|
27330
|
+
const ctor = v.constructor?.name;
|
|
27331
|
+
if (ctor === "Object") {
|
|
27332
|
+
out.push(...flattenInsertPaths(v, path2));
|
|
27333
|
+
}
|
|
27334
|
+
}
|
|
27335
|
+
}
|
|
27336
|
+
return out;
|
|
27337
|
+
}
|
|
27338
|
+
var MONGO_OPERATOR_TIER, MONGO_BROAD_FILTER_OPERATORS, analyzeMongoDmlRisk = (intent, context) => {
|
|
27339
|
+
const factors = [];
|
|
27340
|
+
applyPermission(intent.operation, context, factors);
|
|
27341
|
+
applyTableBlacklist(intent.target, context, factors);
|
|
27342
|
+
if (intent.operation === "insert") {
|
|
27343
|
+
applyColumnBlacklist(intent.target, flattenInsertPaths(intent.data), context, factors);
|
|
27344
|
+
} else if (intent.operation === "update") {
|
|
27345
|
+
const cls = classifyMongoUpdate(intent.set);
|
|
27346
|
+
for (const f of cls.tierFactors)
|
|
27347
|
+
pushFactor2(factors, f.code, f.severity, f.message);
|
|
27348
|
+
applyColumnBlacklist(intent.target, cls.fields, context, factors);
|
|
27349
|
+
const where = intent.where ?? {};
|
|
27350
|
+
if (Object.keys(where).length === 0) {
|
|
27351
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
|
|
27352
|
+
} else if (!hasIdEquality(where)) {
|
|
27353
|
+
if (hasBroadFilter(where)) {
|
|
27354
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
|
|
27355
|
+
} else if (hasNonIdEquality(where)) {
|
|
27356
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
|
|
27357
|
+
} else {
|
|
27358
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
|
|
27359
|
+
}
|
|
27360
|
+
}
|
|
27361
|
+
} else {
|
|
27362
|
+
const where = intent.where ?? {};
|
|
27363
|
+
if (Object.keys(where).length === 0) {
|
|
27364
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
|
|
27365
|
+
} else if (!hasIdEquality(where)) {
|
|
27366
|
+
if (hasBroadFilter(where)) {
|
|
27367
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
|
|
27368
|
+
} else if (hasNonIdEquality(where)) {
|
|
27369
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
|
|
27370
|
+
} else {
|
|
27371
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
|
|
27372
|
+
}
|
|
27373
|
+
}
|
|
27374
|
+
}
|
|
27375
|
+
applySchemaCoverage(intent.target, context, factors);
|
|
27376
|
+
return {
|
|
27377
|
+
decision: decide2(factors),
|
|
27378
|
+
operation: broadOperation(intent.operation),
|
|
27379
|
+
targetTables: [intent.target],
|
|
27380
|
+
riskFactors: factors,
|
|
27381
|
+
recommendations: buildRecommendations2(factors),
|
|
27382
|
+
suggestedCommands: buildSuggestedCommands2(intent.target, factors)
|
|
27383
|
+
};
|
|
27384
|
+
};
|
|
27385
|
+
var init_dml_plan = __esm(() => {
|
|
27386
|
+
init_path_matcher();
|
|
27387
|
+
init_permission_guard();
|
|
27388
|
+
MONGO_OPERATOR_TIER = {
|
|
27389
|
+
$set: "SAFE",
|
|
27390
|
+
$unset: "SAFE",
|
|
27391
|
+
$rename: "RENAME",
|
|
27392
|
+
$inc: "ARITHMETIC",
|
|
27393
|
+
$mul: "ARITHMETIC",
|
|
27394
|
+
$min: "ARITHMETIC",
|
|
27395
|
+
$max: "ARITHMETIC",
|
|
27396
|
+
$currentDate: "ARITHMETIC",
|
|
27397
|
+
$push: "ARRAY",
|
|
27398
|
+
$pull: "ARRAY",
|
|
27399
|
+
$pullAll: "ARRAY",
|
|
27400
|
+
$pop: "ARRAY",
|
|
27401
|
+
$addToSet: "ARRAY",
|
|
27402
|
+
$bit: "BITWISE",
|
|
27403
|
+
$where: "BLOCK"
|
|
27404
|
+
};
|
|
27405
|
+
MONGO_BROAD_FILTER_OPERATORS = new Set([
|
|
27406
|
+
"$regex",
|
|
27407
|
+
"$in",
|
|
27408
|
+
"$nin",
|
|
27409
|
+
"$gt",
|
|
27410
|
+
"$gte",
|
|
27411
|
+
"$lt",
|
|
27412
|
+
"$lte",
|
|
27413
|
+
"$ne",
|
|
27414
|
+
"$exists",
|
|
27415
|
+
"$or",
|
|
27416
|
+
"$and",
|
|
27417
|
+
"$nor",
|
|
27418
|
+
"$not",
|
|
27419
|
+
"$expr",
|
|
27420
|
+
"$text"
|
|
27421
|
+
]);
|
|
27422
|
+
});
|
|
27423
|
+
|
|
26795
27424
|
// src/core/dml-plan-sql.ts
|
|
26796
27425
|
function assertIdentifier(value, role) {
|
|
26797
27426
|
if (!value || !IDENTIFIER_RE.test(value)) {
|
|
@@ -26851,359 +27480,6 @@ var init_dml_plan_sql = __esm(() => {
|
|
|
26851
27480
|
IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
26852
27481
|
});
|
|
26853
27482
|
|
|
26854
|
-
// src/utils/where-parser.ts
|
|
26855
|
-
function parseWhereClause(whereClause) {
|
|
26856
|
-
if (!whereClause || whereClause.trim() === "") {
|
|
26857
|
-
throw new Error("WHERE clause cannot be empty");
|
|
26858
|
-
}
|
|
26859
|
-
const conditions = {};
|
|
26860
|
-
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
26861
|
-
for (const part of andParts) {
|
|
26862
|
-
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
26863
|
-
if (!match) {
|
|
26864
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
26865
|
-
}
|
|
26866
|
-
const column = match[1];
|
|
26867
|
-
const valueStr = match[2];
|
|
26868
|
-
if (valueStr === undefined || column === undefined) {
|
|
26869
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
26870
|
-
}
|
|
26871
|
-
const trimmed = valueStr.trim();
|
|
26872
|
-
const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
|
|
26873
|
-
if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
|
|
26874
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
26875
|
-
}
|
|
26876
|
-
const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
|
|
26877
|
-
let value = stripped;
|
|
26878
|
-
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
26879
|
-
value = Number(stripped);
|
|
26880
|
-
}
|
|
26881
|
-
if (stripped === "true")
|
|
26882
|
-
value = true;
|
|
26883
|
-
else if (stripped === "false")
|
|
26884
|
-
value = false;
|
|
26885
|
-
else if (stripped === "null")
|
|
26886
|
-
value = null;
|
|
26887
|
-
conditions[column] = value;
|
|
26888
|
-
}
|
|
26889
|
-
return conditions;
|
|
26890
|
-
}
|
|
26891
|
-
|
|
26892
|
-
// src/core/mongo/dml-plan.ts
|
|
26893
|
-
function permissionSql(operation) {
|
|
26894
|
-
if (operation === "insert")
|
|
26895
|
-
return "INSERT INTO dummy";
|
|
26896
|
-
if (operation === "update")
|
|
26897
|
-
return "UPDATE dummy";
|
|
26898
|
-
return "DELETE FROM dummy";
|
|
26899
|
-
}
|
|
26900
|
-
function broadOperation(operation) {
|
|
26901
|
-
if (operation === "insert")
|
|
26902
|
-
return "INSERT";
|
|
26903
|
-
if (operation === "update")
|
|
26904
|
-
return "UPDATE";
|
|
26905
|
-
return "DELETE";
|
|
26906
|
-
}
|
|
26907
|
-
function pushFactor2(factors, code, severity, message) {
|
|
26908
|
-
if (factors.some((f) => f.code === code && f.message === message))
|
|
26909
|
-
return;
|
|
26910
|
-
factors.push({ code, severity, message });
|
|
26911
|
-
}
|
|
26912
|
-
function decide2(factors) {
|
|
26913
|
-
if (factors.some((f) => f.severity === "block"))
|
|
26914
|
-
return "BLOCK";
|
|
26915
|
-
if (factors.some((f) => f.severity === "warn"))
|
|
26916
|
-
return "WARN";
|
|
26917
|
-
return "ALLOW";
|
|
26918
|
-
}
|
|
26919
|
-
function classifyMongoUpdate(setDoc) {
|
|
26920
|
-
const fields = new Set;
|
|
26921
|
-
const tierFactors = [];
|
|
26922
|
-
let hasBlock = false;
|
|
26923
|
-
const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
|
|
26924
|
-
if (!hasAnyOperator) {
|
|
26925
|
-
for (const k of Object.keys(setDoc))
|
|
26926
|
-
fields.add(k);
|
|
26927
|
-
return { fields: Array.from(fields), tierFactors, hasBlock };
|
|
26928
|
-
}
|
|
26929
|
-
const seenTiers = new Map;
|
|
26930
|
-
for (const [op, payload] of Object.entries(setDoc)) {
|
|
26931
|
-
if (!op.startsWith("$")) {
|
|
26932
|
-
fields.add(op);
|
|
26933
|
-
continue;
|
|
26934
|
-
}
|
|
26935
|
-
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
26936
|
-
for (const k of Object.keys(payload))
|
|
26937
|
-
fields.add(k);
|
|
26938
|
-
}
|
|
26939
|
-
const tier = MONGO_OPERATOR_TIER[op];
|
|
26940
|
-
if (tier === undefined) {
|
|
26941
|
-
hasBlock = true;
|
|
26942
|
-
tierFactors.push({
|
|
26943
|
-
code: "mongo_unknown_operator",
|
|
26944
|
-
severity: "block",
|
|
26945
|
-
message: `Update uses unknown operator '${op}'. Reject by default; add to tier table if intentional.`
|
|
26946
|
-
});
|
|
26947
|
-
continue;
|
|
26948
|
-
}
|
|
26949
|
-
if (tier === "BLOCK") {
|
|
26950
|
-
hasBlock = true;
|
|
26951
|
-
tierFactors.push({
|
|
26952
|
-
code: "mongo_unknown_operator",
|
|
26953
|
-
severity: "block",
|
|
26954
|
-
message: `Update uses '${op}' which executes server-side code. Operation rejected.`
|
|
26955
|
-
});
|
|
26956
|
-
continue;
|
|
26957
|
-
}
|
|
26958
|
-
if (tier === "SAFE")
|
|
26959
|
-
continue;
|
|
26960
|
-
const bucket = seenTiers.get(tier) ?? [];
|
|
26961
|
-
bucket.push(op);
|
|
26962
|
-
seenTiers.set(tier, bucket);
|
|
26963
|
-
}
|
|
26964
|
-
for (const [tier, ops] of seenTiers) {
|
|
26965
|
-
if (tier === "RENAME") {
|
|
26966
|
-
tierFactors.push({
|
|
26967
|
-
code: "mongo_rename_operator",
|
|
26968
|
-
severity: "warn",
|
|
26969
|
-
message: `Update uses ${ops.join(", ")}; field rename does not exfiltrate data but can break readers.`
|
|
26970
|
-
});
|
|
26971
|
-
} else if (tier === "ARITHMETIC") {
|
|
26972
|
-
tierFactors.push({
|
|
26973
|
-
code: "mongo_arithmetic_operator",
|
|
26974
|
-
severity: "warn",
|
|
26975
|
-
message: `Update uses ${ops.join(", ")}; numeric mutation may compound silently.`
|
|
26976
|
-
});
|
|
26977
|
-
} else if (tier === "ARRAY") {
|
|
26978
|
-
tierFactors.push({
|
|
26979
|
-
code: "mongo_array_operator",
|
|
26980
|
-
severity: "warn",
|
|
26981
|
-
message: `Update uses ${ops.join(", ")}; array mutation can grow unboundedly without a size guard.`
|
|
26982
|
-
});
|
|
26983
|
-
} else if (tier === "BITWISE") {
|
|
26984
|
-
tierFactors.push({
|
|
26985
|
-
code: "mongo_bitwise_operator",
|
|
26986
|
-
severity: "warn",
|
|
26987
|
-
message: `Update uses ${ops.join(", ")}; bitwise updates skip type promotion checks.`
|
|
26988
|
-
});
|
|
26989
|
-
}
|
|
26990
|
-
}
|
|
26991
|
-
return { fields: Array.from(fields), tierFactors, hasBlock };
|
|
26992
|
-
}
|
|
26993
|
-
function hasIdEquality(where) {
|
|
26994
|
-
if (!("_id" in where))
|
|
26995
|
-
return false;
|
|
26996
|
-
const value = where._id;
|
|
26997
|
-
if (value === null || value === undefined)
|
|
26998
|
-
return false;
|
|
26999
|
-
if (typeof value === "object")
|
|
27000
|
-
return false;
|
|
27001
|
-
return true;
|
|
27002
|
-
}
|
|
27003
|
-
function hasNonIdEquality(where) {
|
|
27004
|
-
for (const [key, value] of Object.entries(where)) {
|
|
27005
|
-
if (key === "_id")
|
|
27006
|
-
continue;
|
|
27007
|
-
if (key.startsWith("$"))
|
|
27008
|
-
continue;
|
|
27009
|
-
if (value === null)
|
|
27010
|
-
continue;
|
|
27011
|
-
if (typeof value === "object")
|
|
27012
|
-
continue;
|
|
27013
|
-
return true;
|
|
27014
|
-
}
|
|
27015
|
-
return false;
|
|
27016
|
-
}
|
|
27017
|
-
function hasBroadFilter(where) {
|
|
27018
|
-
for (const [key, value] of Object.entries(where)) {
|
|
27019
|
-
if (MONGO_BROAD_FILTER_OPERATORS.has(key))
|
|
27020
|
-
return true;
|
|
27021
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
27022
|
-
for (const inner of Object.keys(value)) {
|
|
27023
|
-
if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
|
|
27024
|
-
return true;
|
|
27025
|
-
}
|
|
27026
|
-
}
|
|
27027
|
-
}
|
|
27028
|
-
return false;
|
|
27029
|
-
}
|
|
27030
|
-
function applyPermission(operation, context, factors) {
|
|
27031
|
-
const permResult = checkPermission(permissionSql(operation), context.permission);
|
|
27032
|
-
if (!permResult.allowed) {
|
|
27033
|
-
pushFactor2(factors, "permission_denied", "block", permResult.reason);
|
|
27034
|
-
}
|
|
27035
|
-
}
|
|
27036
|
-
function applyTableBlacklist(target, context, factors) {
|
|
27037
|
-
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
27038
|
-
if (blacklisted.includes(target.toLowerCase())) {
|
|
27039
|
-
pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
|
|
27040
|
-
}
|
|
27041
|
-
}
|
|
27042
|
-
function applyColumnBlacklist(target, fields, context, factors) {
|
|
27043
|
-
const columns = context.blacklist.columns ?? {};
|
|
27044
|
-
const lower = target.toLowerCase();
|
|
27045
|
-
let raw = [];
|
|
27046
|
-
for (const [t2, cols] of Object.entries(columns)) {
|
|
27047
|
-
if (t2.toLowerCase() === lower) {
|
|
27048
|
-
raw = cols;
|
|
27049
|
-
break;
|
|
27050
|
-
}
|
|
27051
|
-
}
|
|
27052
|
-
if (raw.length === 0)
|
|
27053
|
-
return;
|
|
27054
|
-
const { patterns } = compilePatterns(raw);
|
|
27055
|
-
if (patterns.length === 0)
|
|
27056
|
-
return;
|
|
27057
|
-
for (const field of fields) {
|
|
27058
|
-
if (matchAny(field, patterns)) {
|
|
27059
|
-
pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted path ${target}.${field}.`);
|
|
27060
|
-
}
|
|
27061
|
-
}
|
|
27062
|
-
}
|
|
27063
|
-
function applySchemaCoverage(target, context, factors) {
|
|
27064
|
-
const schemaTables = Object.keys(context.schema);
|
|
27065
|
-
if (schemaTables.length === 0) {
|
|
27066
|
-
pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
|
|
27067
|
-
return;
|
|
27068
|
-
}
|
|
27069
|
-
const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
27070
|
-
if (!known) {
|
|
27071
|
-
pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
|
|
27072
|
-
}
|
|
27073
|
-
}
|
|
27074
|
-
function buildRecommendations2(factors) {
|
|
27075
|
-
const out = new Set;
|
|
27076
|
-
const codes = new Set(factors.map((f) => f.code));
|
|
27077
|
-
if (codes.has("nonsql_filter_empty")) {
|
|
27078
|
-
out.add("Add an _id equality filter before executing this MongoDB write.");
|
|
27079
|
-
}
|
|
27080
|
-
if (codes.has("nonsql_missing_id")) {
|
|
27081
|
-
out.add("Prefer an _id equality filter before executing this MongoDB write.");
|
|
27082
|
-
}
|
|
27083
|
-
if (codes.has("nonsql_filter_broad")) {
|
|
27084
|
-
out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
|
|
27085
|
-
}
|
|
27086
|
-
if (codes.has("nonsql_unsupported_operator")) {
|
|
27087
|
-
out.add("Restrict the update document to $set / $unset for planner-safe writes.");
|
|
27088
|
-
}
|
|
27089
|
-
if (codes.has("permission_denied")) {
|
|
27090
|
-
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
27091
|
-
}
|
|
27092
|
-
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
27093
|
-
out.add("Review blacklist rules before accessing sensitive data.");
|
|
27094
|
-
}
|
|
27095
|
-
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
27096
|
-
out.add("Refresh schema cache for the target collection before executing.");
|
|
27097
|
-
}
|
|
27098
|
-
out.add("Use --dry-run on the actual write command.");
|
|
27099
|
-
return Array.from(out);
|
|
27100
|
-
}
|
|
27101
|
-
function buildSuggestedCommands2(target, factors) {
|
|
27102
|
-
const codes = new Set(factors.map((f) => f.code));
|
|
27103
|
-
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
27104
|
-
return [`dbcli schema ${target} --format json`];
|
|
27105
|
-
}
|
|
27106
|
-
return [];
|
|
27107
|
-
}
|
|
27108
|
-
function flattenInsertPaths(data, prefix = "") {
|
|
27109
|
-
const out = [];
|
|
27110
|
-
for (const [k, v] of Object.entries(data)) {
|
|
27111
|
-
const path2 = prefix === "" ? k : `${prefix}.${k}`;
|
|
27112
|
-
out.push(path2);
|
|
27113
|
-
if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
|
|
27114
|
-
const ctor = v.constructor?.name;
|
|
27115
|
-
if (ctor === "Object") {
|
|
27116
|
-
out.push(...flattenInsertPaths(v, path2));
|
|
27117
|
-
}
|
|
27118
|
-
}
|
|
27119
|
-
}
|
|
27120
|
-
return out;
|
|
27121
|
-
}
|
|
27122
|
-
var MONGO_OPERATOR_TIER, MONGO_BROAD_FILTER_OPERATORS, analyzeMongoDmlRisk = (intent, context) => {
|
|
27123
|
-
const factors = [];
|
|
27124
|
-
applyPermission(intent.operation, context, factors);
|
|
27125
|
-
applyTableBlacklist(intent.target, context, factors);
|
|
27126
|
-
if (intent.operation === "insert") {
|
|
27127
|
-
applyColumnBlacklist(intent.target, flattenInsertPaths(intent.data), context, factors);
|
|
27128
|
-
} else if (intent.operation === "update") {
|
|
27129
|
-
const cls = classifyMongoUpdate(intent.set);
|
|
27130
|
-
for (const f of cls.tierFactors)
|
|
27131
|
-
pushFactor2(factors, f.code, f.severity, f.message);
|
|
27132
|
-
applyColumnBlacklist(intent.target, cls.fields, context, factors);
|
|
27133
|
-
const where = intent.where ?? {};
|
|
27134
|
-
if (Object.keys(where).length === 0) {
|
|
27135
|
-
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
|
|
27136
|
-
} else if (!hasIdEquality(where)) {
|
|
27137
|
-
if (hasBroadFilter(where)) {
|
|
27138
|
-
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
|
|
27139
|
-
} else if (hasNonIdEquality(where)) {
|
|
27140
|
-
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
|
|
27141
|
-
} else {
|
|
27142
|
-
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
|
|
27143
|
-
}
|
|
27144
|
-
}
|
|
27145
|
-
} else {
|
|
27146
|
-
const where = intent.where ?? {};
|
|
27147
|
-
if (Object.keys(where).length === 0) {
|
|
27148
|
-
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
|
|
27149
|
-
} else if (!hasIdEquality(where)) {
|
|
27150
|
-
if (hasBroadFilter(where)) {
|
|
27151
|
-
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
|
|
27152
|
-
} else if (hasNonIdEquality(where)) {
|
|
27153
|
-
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
|
|
27154
|
-
} else {
|
|
27155
|
-
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
|
|
27156
|
-
}
|
|
27157
|
-
}
|
|
27158
|
-
}
|
|
27159
|
-
applySchemaCoverage(intent.target, context, factors);
|
|
27160
|
-
return {
|
|
27161
|
-
decision: decide2(factors),
|
|
27162
|
-
operation: broadOperation(intent.operation),
|
|
27163
|
-
targetTables: [intent.target],
|
|
27164
|
-
riskFactors: factors,
|
|
27165
|
-
recommendations: buildRecommendations2(factors),
|
|
27166
|
-
suggestedCommands: buildSuggestedCommands2(intent.target, factors)
|
|
27167
|
-
};
|
|
27168
|
-
};
|
|
27169
|
-
var init_dml_plan = __esm(() => {
|
|
27170
|
-
init_permission_guard();
|
|
27171
|
-
MONGO_OPERATOR_TIER = {
|
|
27172
|
-
$set: "SAFE",
|
|
27173
|
-
$unset: "SAFE",
|
|
27174
|
-
$rename: "RENAME",
|
|
27175
|
-
$inc: "ARITHMETIC",
|
|
27176
|
-
$mul: "ARITHMETIC",
|
|
27177
|
-
$min: "ARITHMETIC",
|
|
27178
|
-
$max: "ARITHMETIC",
|
|
27179
|
-
$currentDate: "ARITHMETIC",
|
|
27180
|
-
$push: "ARRAY",
|
|
27181
|
-
$pull: "ARRAY",
|
|
27182
|
-
$pullAll: "ARRAY",
|
|
27183
|
-
$pop: "ARRAY",
|
|
27184
|
-
$addToSet: "ARRAY",
|
|
27185
|
-
$bit: "BITWISE",
|
|
27186
|
-
$where: "BLOCK"
|
|
27187
|
-
};
|
|
27188
|
-
MONGO_BROAD_FILTER_OPERATORS = new Set([
|
|
27189
|
-
"$regex",
|
|
27190
|
-
"$in",
|
|
27191
|
-
"$nin",
|
|
27192
|
-
"$gt",
|
|
27193
|
-
"$gte",
|
|
27194
|
-
"$lt",
|
|
27195
|
-
"$lte",
|
|
27196
|
-
"$ne",
|
|
27197
|
-
"$exists",
|
|
27198
|
-
"$or",
|
|
27199
|
-
"$and",
|
|
27200
|
-
"$nor",
|
|
27201
|
-
"$not",
|
|
27202
|
-
"$expr",
|
|
27203
|
-
"$text"
|
|
27204
|
-
]);
|
|
27205
|
-
});
|
|
27206
|
-
|
|
27207
27483
|
// src/core/redis/dml-plan.ts
|
|
27208
27484
|
function permissionSql2(operation) {
|
|
27209
27485
|
if (operation === "insert")
|
|
@@ -27665,7 +27941,7 @@ async function insertCommand(table, options, command) {
|
|
|
27665
27941
|
const blacklistManager = new BlacklistManager(config);
|
|
27666
27942
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
27667
27943
|
blacklistValidator.checkTableBlacklist("INSERT", table, []);
|
|
27668
|
-
blacklistValidator.checkColumnBlacklistOnWrite(table,
|
|
27944
|
+
blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
|
|
27669
27945
|
const preview = `SET ${table} ... (Redis Insert)`;
|
|
27670
27946
|
if (options.dryRun) {
|
|
27671
27947
|
const output = {
|
|
@@ -27730,7 +28006,7 @@ async function insertCommand(table, options, command) {
|
|
|
27730
28006
|
const blacklistManager = new BlacklistManager(config);
|
|
27731
28007
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
27732
28008
|
blacklistValidator.checkTableBlacklist("INSERT", table, []);
|
|
27733
|
-
blacklistValidator.checkColumnBlacklistOnWrite(table,
|
|
28009
|
+
blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
|
|
27734
28010
|
const preview = previewInsert(table, data);
|
|
27735
28011
|
if (options.dryRun) {
|
|
27736
28012
|
const output = {
|
|
@@ -27848,9 +28124,11 @@ var init_insert = __esm(() => {
|
|
|
27848
28124
|
init_mutation_outcome();
|
|
27849
28125
|
init_config();
|
|
27850
28126
|
init_permission_guard();
|
|
28127
|
+
init_blacklist_manager();
|
|
27851
28128
|
init_blacklist_validator();
|
|
27852
28129
|
init_blacklist();
|
|
27853
28130
|
init_config_path();
|
|
28131
|
+
init_dml_plan();
|
|
27854
28132
|
init_dml_plan4();
|
|
27855
28133
|
init_integration_helper();
|
|
27856
28134
|
});
|
|
@@ -28008,7 +28286,11 @@ async function updateCommand(table, options, command) {
|
|
|
28008
28286
|
if (hasOperator) {
|
|
28009
28287
|
const operators = updateDoc;
|
|
28010
28288
|
for (const [op, payload] of Object.entries(operators)) {
|
|
28011
|
-
if ((
|
|
28289
|
+
if (!op.startsWith("$")) {
|
|
28290
|
+
writtenFields.add(op);
|
|
28291
|
+
continue;
|
|
28292
|
+
}
|
|
28293
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
28012
28294
|
for (const k of Object.keys(payload))
|
|
28013
28295
|
writtenFields.add(k);
|
|
28014
28296
|
}
|
|
@@ -28169,6 +28451,7 @@ var init_update = __esm(() => {
|
|
|
28169
28451
|
init_mutation_outcome();
|
|
28170
28452
|
init_config();
|
|
28171
28453
|
init_permission_guard();
|
|
28454
|
+
init_blacklist_manager();
|
|
28172
28455
|
init_blacklist_validator();
|
|
28173
28456
|
init_blacklist();
|
|
28174
28457
|
init_config_path();
|
|
@@ -28468,6 +28751,7 @@ var init_delete = __esm(() => {
|
|
|
28468
28751
|
init_mutation_outcome();
|
|
28469
28752
|
init_config();
|
|
28470
28753
|
init_permission_guard();
|
|
28754
|
+
init_blacklist_manager();
|
|
28471
28755
|
init_blacklist_validator();
|
|
28472
28756
|
init_blacklist();
|
|
28473
28757
|
init_config_path();
|
|
@@ -28911,6 +29195,7 @@ var init_export = __esm(() => {
|
|
|
28911
29195
|
init_config();
|
|
28912
29196
|
init_prompts();
|
|
28913
29197
|
init_config_path();
|
|
29198
|
+
init_blacklist_manager();
|
|
28914
29199
|
init_blacklist_validator();
|
|
28915
29200
|
init_applied_limit();
|
|
28916
29201
|
init_integration_helper();
|
|
@@ -30147,6 +30432,7 @@ async function gatherContext(workspaceRoot, configPath, options = {}) {
|
|
|
30147
30432
|
}
|
|
30148
30433
|
var init_context = __esm(() => {
|
|
30149
30434
|
init_config();
|
|
30435
|
+
init_blacklist_manager();
|
|
30150
30436
|
init_loader();
|
|
30151
30437
|
init_snippet_paths();
|
|
30152
30438
|
init_semantic();
|
|
@@ -31363,11 +31649,8 @@ function auditBlacklistPatterns(cfg) {
|
|
|
31363
31649
|
}
|
|
31364
31650
|
return { warnings };
|
|
31365
31651
|
}
|
|
31366
|
-
function isValidTableNameForSystem(name,
|
|
31367
|
-
|
|
31368
|
-
return false;
|
|
31369
|
-
const literalMatching = system === "postgresql" || system === "mysql" || system === "mariadb" || system === "mongodb";
|
|
31370
|
-
return !(literalMatching && GLOB_CHARS.test(name));
|
|
31652
|
+
function isValidTableNameForSystem(name, _system) {
|
|
31653
|
+
return isValidTableName(name);
|
|
31371
31654
|
}
|
|
31372
31655
|
function isValidTableName(name) {
|
|
31373
31656
|
return VALID_TABLE_NAME.test(name);
|
|
@@ -31533,16 +31816,16 @@ function resolveBlacklistConfigPath(options, command) {
|
|
|
31533
31816
|
}
|
|
31534
31817
|
return DEFAULT_CONFIG_PATH;
|
|
31535
31818
|
}
|
|
31536
|
-
var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME,
|
|
31819
|
+
var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME, blacklistCommand, tableCmd, columnCmd;
|
|
31537
31820
|
var init_blacklist2 = __esm(() => {
|
|
31538
31821
|
init_config_v2();
|
|
31539
31822
|
init_esm();
|
|
31540
31823
|
init_message_loader();
|
|
31541
31824
|
init_config();
|
|
31825
|
+
init_path_matcher();
|
|
31542
31826
|
init_validation();
|
|
31543
|
-
VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]
|
|
31544
|
-
VALID_COLUMN_NAME = /^[a-zA-
|
|
31545
|
-
GLOB_CHARS = /[*?[\]]/;
|
|
31827
|
+
VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]\\-]+$/;
|
|
31828
|
+
VALID_COLUMN_NAME = /^[a-zA-Z0-9_*?[\]\\-]+$/;
|
|
31546
31829
|
blacklistCommand = new Command("blacklist").description(t("blacklist.description"));
|
|
31547
31830
|
blacklistCommand.command("list").description(t("blacklist.list_title")).option("--config <path>", "Path to .dbcli config file").option("--format <type>", "Output format: text, json", "text").action(async (options, command) => {
|
|
31548
31831
|
try {
|
|
@@ -31718,6 +32001,7 @@ var init_check = __esm(() => {
|
|
|
31718
32001
|
init_adapters();
|
|
31719
32002
|
init_config();
|
|
31720
32003
|
init_health_checker();
|
|
32004
|
+
init_blacklist_manager();
|
|
31721
32005
|
init_size_category();
|
|
31722
32006
|
init_validation();
|
|
31723
32007
|
init_connection_selector();
|
|
@@ -33594,9 +33878,8 @@ var init_proposals = __esm(() => {
|
|
|
33594
33878
|
});
|
|
33595
33879
|
|
|
33596
33880
|
// src/core/orm-drift/compare.ts
|
|
33597
|
-
function
|
|
33598
|
-
|
|
33599
|
-
return new RegExp(`^${escaped}$`);
|
|
33881
|
+
function starOnlyGlob(glob) {
|
|
33882
|
+
return glob.replace(/[?[\]\\]/g, "\\$&");
|
|
33600
33883
|
}
|
|
33601
33884
|
function tableMap(schema, defaultSchema) {
|
|
33602
33885
|
const tables = new Map;
|
|
@@ -33630,7 +33913,7 @@ function compareNormalized(orm, db, opts) {
|
|
|
33630
33913
|
const tableKeys = new Set([...ormTables.keys(), ...dbTables.keys()]);
|
|
33631
33914
|
const extraDefaultIgnore = opts.extraDefaultIgnore ?? [];
|
|
33632
33915
|
const defaultIgnore = [...DEFAULT_IGNORE, ...extraDefaultIgnore];
|
|
33633
|
-
const ignorePatterns = opts.ignore
|
|
33916
|
+
const ignorePatterns = opts.ignore;
|
|
33634
33917
|
const targetLabel = opts.targetLabel ?? "database";
|
|
33635
33918
|
const entries = [];
|
|
33636
33919
|
for (const tableKey of tableKeys) {
|
|
@@ -33641,7 +33924,7 @@ function compareNormalized(orm, db, opts) {
|
|
|
33641
33924
|
continue;
|
|
33642
33925
|
const table = qualifiedTableName(normalizedTable.identity);
|
|
33643
33926
|
const isDefaultIgnored = defaultIgnore.includes(normalizedTable.identity.table);
|
|
33644
|
-
if (isDefaultIgnored || ignorePatterns.some((pattern) => pattern
|
|
33927
|
+
if (isDefaultIgnored || ignorePatterns.some((pattern) => globMatches(starOnlyGlob(pattern), table))) {
|
|
33645
33928
|
entries.push(entryWithProposals({
|
|
33646
33929
|
category: "unmanaged",
|
|
33647
33930
|
severity: "info",
|
|
@@ -33823,6 +34106,7 @@ function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource,
|
|
|
33823
34106
|
var DEFAULT_IGNORE, entryOrder = (left, right) => codePointOrder(left.table, right.table) || codePointOrder(left.object, right.object) || codePointOrder(left.category, right.category) || codePointOrder(left.detail, right.detail);
|
|
33824
34107
|
var init_compare = __esm(() => {
|
|
33825
34108
|
init_normalized_schema();
|
|
34109
|
+
init_glob();
|
|
33826
34110
|
init_proposals();
|
|
33827
34111
|
DEFAULT_IGNORE = ["_prisma_migrations"];
|
|
33828
34112
|
});
|
|
@@ -34719,7 +35003,7 @@ function shouldEmbedRecent(opts) {
|
|
|
34719
35003
|
return opts.forAgent === true || opts.format === "json";
|
|
34720
35004
|
}
|
|
34721
35005
|
function briefifyForRecent(entry) {
|
|
34722
|
-
const phase = entry.metadata?.
|
|
35006
|
+
const phase = entry.metadata?.shell_phase;
|
|
34723
35007
|
return {
|
|
34724
35008
|
id: entry.id,
|
|
34725
35009
|
ts: entry.ts,
|
|
@@ -35426,6 +35710,7 @@ var init_collector2 = __esm(() => {
|
|
|
35426
35710
|
init_saved_queries();
|
|
35427
35711
|
init_select_snippets();
|
|
35428
35712
|
init_run_diagnostic();
|
|
35713
|
+
init_blacklist_manager();
|
|
35429
35714
|
init_blacklist_validator();
|
|
35430
35715
|
init_section_map();
|
|
35431
35716
|
init_types7();
|
|
@@ -37431,7 +37716,7 @@ function parseTailN(raw) {
|
|
|
37431
37716
|
return requested;
|
|
37432
37717
|
}
|
|
37433
37718
|
function briefify(entry) {
|
|
37434
|
-
const phase = entry.metadata?.
|
|
37719
|
+
const phase = entry.metadata?.shell_phase;
|
|
37435
37720
|
return {
|
|
37436
37721
|
ts: entry.ts,
|
|
37437
37722
|
command: entry.command,
|
|
@@ -37486,8 +37771,8 @@ function renderTailAllTable(envelopes) {
|
|
|
37486
37771
|
}
|
|
37487
37772
|
function briefifyShow(entry) {
|
|
37488
37773
|
const { metadata, redacted_query, ...rest } = entry;
|
|
37489
|
-
const phase = metadata?.
|
|
37490
|
-
return phase === undefined ? rest : { ...rest, metadata: {
|
|
37774
|
+
const phase = metadata?.shell_phase;
|
|
37775
|
+
return phase === undefined ? rest : { ...rest, metadata: { shell_phase: phase } };
|
|
37491
37776
|
}
|
|
37492
37777
|
function renderEntryTable(entry) {
|
|
37493
37778
|
const lines = [];
|
|
@@ -42015,6 +42300,7 @@ var init_snapshot = __esm(() => {
|
|
|
42015
42300
|
init_config();
|
|
42016
42301
|
init_config_path();
|
|
42017
42302
|
init_validation();
|
|
42303
|
+
init_blacklist_manager();
|
|
42018
42304
|
init_blacklist_validator();
|
|
42019
42305
|
init_query_executor();
|
|
42020
42306
|
init_permission_guard();
|
|
@@ -42683,6 +42969,7 @@ var init_assert = __esm(() => {
|
|
|
42683
42969
|
init_config();
|
|
42684
42970
|
init_config_path();
|
|
42685
42971
|
init_validation();
|
|
42972
|
+
init_blacklist_manager();
|
|
42686
42973
|
init_blacklist_validator();
|
|
42687
42974
|
init_query_executor();
|
|
42688
42975
|
init_saved_queries();
|
|
@@ -44861,6 +45148,7 @@ var init_verify2 = __esm(() => {
|
|
|
44861
45148
|
init_adapters();
|
|
44862
45149
|
init_config();
|
|
44863
45150
|
init_config_path();
|
|
45151
|
+
init_blacklist_manager();
|
|
44864
45152
|
init_blacklist_validator();
|
|
44865
45153
|
init_query_executor();
|
|
44866
45154
|
init_query_risk_analyzer();
|
|
@@ -45612,15 +45900,17 @@ class ReplEngine {
|
|
|
45612
45900
|
adapter;
|
|
45613
45901
|
context;
|
|
45614
45902
|
writeGate;
|
|
45903
|
+
auditSink;
|
|
45615
45904
|
state;
|
|
45616
45905
|
buffer;
|
|
45617
45906
|
history;
|
|
45618
45907
|
formatter;
|
|
45619
45908
|
config;
|
|
45620
|
-
constructor(adapter, context, historyPath, config = null, writeGate = null) {
|
|
45909
|
+
constructor(adapter, context, historyPath, config = null, writeGate = null, auditSink = null) {
|
|
45621
45910
|
this.adapter = adapter;
|
|
45622
45911
|
this.context = context;
|
|
45623
45912
|
this.writeGate = writeGate;
|
|
45913
|
+
this.auditSink = auditSink;
|
|
45624
45914
|
this.state = { format: "table", timing: false, connected: true, noLimit: false };
|
|
45625
45915
|
this.buffer = new MultilineBuffer;
|
|
45626
45916
|
this.history = new HistoryManager(historyPath);
|
|
@@ -45752,10 +46042,11 @@ class ReplEngine {
|
|
|
45752
46042
|
} else {
|
|
45753
46043
|
const permResult = checkPermission(sql, this.context.permission, SQL_DIALECTS.find((dialect) => dialect === this.context.system));
|
|
45754
46044
|
if (!permResult.allowed) {
|
|
46045
|
+
await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
|
|
45755
46046
|
return {
|
|
45756
46047
|
action: "continue",
|
|
45757
46048
|
output: import_picocolors3.default.red(t_vars("shell.error_permission", {
|
|
45758
|
-
required: permResult.
|
|
46049
|
+
required: permResult.requiredPermission ?? "admin",
|
|
45759
46050
|
current: this.context.permission
|
|
45760
46051
|
}))
|
|
45761
46052
|
};
|
|
@@ -45771,18 +46062,22 @@ class ReplEngine {
|
|
|
45771
46062
|
} catch (error) {
|
|
45772
46063
|
if (!(error instanceof BlacklistError))
|
|
45773
46064
|
throw error;
|
|
46065
|
+
await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
|
|
45774
46066
|
return {
|
|
45775
46067
|
action: "continue",
|
|
45776
46068
|
output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: error.message }))
|
|
45777
46069
|
};
|
|
45778
46070
|
}
|
|
45779
46071
|
}
|
|
45780
|
-
if (this.writeGate && !await this.writeGate(sql))
|
|
46072
|
+
if (this.writeGate && !await this.writeGate(sql)) {
|
|
46073
|
+
await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
|
|
45781
46074
|
return { action: "continue" };
|
|
46075
|
+
}
|
|
45782
46076
|
return this.runStatement(sql, blacklistValidator, referencedTables);
|
|
45783
46077
|
}
|
|
45784
46078
|
async runStatement(sql, blacklistValidator, referencedTables) {
|
|
45785
46079
|
const startTime = Date.now();
|
|
46080
|
+
await this.auditSink?.({ phase: "attempt", success: true, statement: sql });
|
|
45786
46081
|
try {
|
|
45787
46082
|
const result = await this.adapter.execute(sql, undefined, {
|
|
45788
46083
|
noLimit: this.state.noLimit
|
|
@@ -45808,6 +46103,7 @@ class ReplEngine {
|
|
|
45808
46103
|
` + import_picocolors3.default.dim(t_vars("shell.timing_display", { ms: String(elapsed) }));
|
|
45809
46104
|
}
|
|
45810
46105
|
this.state = { ...this.state, connected: true };
|
|
46106
|
+
await this.auditSink?.({ phase: "outcome", success: true, statement: sql });
|
|
45811
46107
|
return { action: "continue", output };
|
|
45812
46108
|
} catch (error) {
|
|
45813
46109
|
if (this.isConnectionError(error) && this.state.connected) {
|
|
@@ -45819,12 +46115,14 @@ class ReplEngine {
|
|
|
45819
46115
|
console.error(import_picocolors3.default.green(t("shell.error_reconnect_success")));
|
|
45820
46116
|
return this.runStatement(sql, blacklistValidator, referencedTables);
|
|
45821
46117
|
} catch (reconnectError) {
|
|
46118
|
+
await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
|
|
45822
46119
|
return {
|
|
45823
46120
|
action: "continue",
|
|
45824
46121
|
output: import_picocolors3.default.red(t_vars("shell.error_reconnect_failed", { message: reconnectError.message }))
|
|
45825
46122
|
};
|
|
45826
46123
|
}
|
|
45827
46124
|
}
|
|
46125
|
+
await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
|
|
45828
46126
|
return {
|
|
45829
46127
|
action: "continue",
|
|
45830
46128
|
output: import_picocolors3.default.red(t_vars("shell.error_sql_failed", { message: error.message }))
|
|
@@ -45859,6 +46157,7 @@ var init_repl_engine = __esm(() => {
|
|
|
45859
46157
|
init_query_result_formatter();
|
|
45860
46158
|
init_message_loader();
|
|
45861
46159
|
init_sql_tables();
|
|
46160
|
+
init_blacklist_manager();
|
|
45862
46161
|
init_blacklist_validator();
|
|
45863
46162
|
init_blacklist();
|
|
45864
46163
|
import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
@@ -46400,7 +46699,7 @@ async function executeEsBlock(block2, session) {
|
|
|
46400
46699
|
target: record4.target,
|
|
46401
46700
|
sql: record4.statement,
|
|
46402
46701
|
sideEffectTier: record4.tierOverride,
|
|
46403
|
-
metadata: {
|
|
46702
|
+
metadata: { shell_phase: record4.phase }
|
|
46404
46703
|
})
|
|
46405
46704
|
});
|
|
46406
46705
|
console.log(JSON.stringify(res, null, 2));
|
|
@@ -46600,6 +46899,25 @@ var init_shell_write_gate = __esm(() => {
|
|
|
46600
46899
|
init_write_gate_guard();
|
|
46601
46900
|
});
|
|
46602
46901
|
|
|
46902
|
+
// src/commands/shell-audit-sink.ts
|
|
46903
|
+
var exports_shell_audit_sink = {};
|
|
46904
|
+
__export(exports_shell_audit_sink, {
|
|
46905
|
+
createShellAuditSink: () => createShellAuditSink
|
|
46906
|
+
});
|
|
46907
|
+
function createShellAuditSink(options) {
|
|
46908
|
+
const write = options.write ?? writeAuditEntryResult;
|
|
46909
|
+
return async (record4) => {
|
|
46910
|
+
return await write(options.config, "shell", { config: options.configPath }, {
|
|
46911
|
+
success: record4.success,
|
|
46912
|
+
sql: record4.statement,
|
|
46913
|
+
metadata: { shell_phase: record4.phase }
|
|
46914
|
+
});
|
|
46915
|
+
};
|
|
46916
|
+
}
|
|
46917
|
+
var init_shell_audit_sink = __esm(() => {
|
|
46918
|
+
init_integration_helper();
|
|
46919
|
+
});
|
|
46920
|
+
|
|
46603
46921
|
// src/commands/shell.ts
|
|
46604
46922
|
var exports_shell = {};
|
|
46605
46923
|
__export(exports_shell, {
|
|
@@ -46717,7 +47035,11 @@ async function runShell(options, configPath) {
|
|
|
46717
47035
|
dialect,
|
|
46718
47036
|
ask: asker.ask
|
|
46719
47037
|
}) : null;
|
|
46720
|
-
const
|
|
47038
|
+
const auditSink = (await Promise.resolve().then(() => (init_shell_audit_sink(), exports_shell_audit_sink))).createShellAuditSink({
|
|
47039
|
+
config,
|
|
47040
|
+
configPath
|
|
47041
|
+
});
|
|
47042
|
+
const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate, auditSink);
|
|
46721
47043
|
const complete = createCompleter(context);
|
|
46722
47044
|
console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
|
|
46723
47045
|
system: config.connection.system,
|
|
@@ -47549,6 +47871,7 @@ var init_migrate = __esm(() => {
|
|
|
47549
47871
|
init_ddl2();
|
|
47550
47872
|
init_ddl_executor();
|
|
47551
47873
|
init_mutation_confirm();
|
|
47874
|
+
init_blacklist_manager();
|
|
47552
47875
|
init_config_path();
|
|
47553
47876
|
migrateCommand = new Command("migrate").description(t("migrate.description"));
|
|
47554
47877
|
addExecOpts(migrateCommand.command("create <table>").description(t("migrate.create_description")).option("--column <spec...>", 'Column definitions (e.g., "id:serial:pk" "name:varchar(50):not-null")')).action(async (table, opts, command) => {
|
|
@@ -47851,7 +48174,9 @@ function envVarNameFor(connName, field) {
|
|
|
47851
48174
|
const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
47852
48175
|
return `DBCLI_${slug}_${field.toUpperCase()}`;
|
|
47853
48176
|
}
|
|
47854
|
-
var init_config_v2_mutations = () => {
|
|
48177
|
+
var init_config_v2_mutations = __esm(() => {
|
|
48178
|
+
init_validation();
|
|
48179
|
+
});
|
|
47855
48180
|
|
|
47856
48181
|
// src/core/env-file-writer.ts
|
|
47857
48182
|
import { chmod as chmod2, mkdir as mkdir18, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
|
|
@@ -52231,6 +52556,9 @@ var init_data_access = __esm(() => {
|
|
|
52231
52556
|
});
|
|
52232
52557
|
|
|
52233
52558
|
// src/core/orm-drift/change-set.ts
|
|
52559
|
+
function starOnlyGlob2(glob) {
|
|
52560
|
+
return glob.replace(/[?[\]\\]/g, "\\$&");
|
|
52561
|
+
}
|
|
52234
52562
|
function normalizeProposedChanges(input) {
|
|
52235
52563
|
validateInput(input);
|
|
52236
52564
|
const gaps = [
|
|
@@ -52288,7 +52616,7 @@ function tableMap2(schema, defaultSchema, side) {
|
|
|
52288
52616
|
return tables;
|
|
52289
52617
|
}
|
|
52290
52618
|
function ignoredTableKeys(input, declared, baseline, gaps) {
|
|
52291
|
-
const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []]
|
|
52619
|
+
const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []];
|
|
52292
52620
|
const ignored = new Set;
|
|
52293
52621
|
for (const [side, tables] of [
|
|
52294
52622
|
["declared", declared],
|
|
@@ -52296,7 +52624,7 @@ function ignoredTableKeys(input, declared, baseline, gaps) {
|
|
|
52296
52624
|
]) {
|
|
52297
52625
|
for (const [key, resolved] of tables) {
|
|
52298
52626
|
const name = qualifiedTableName(resolved.identity);
|
|
52299
|
-
if (!patterns.some((pattern) => pattern
|
|
52627
|
+
if (!patterns.some((pattern) => globMatches(starOnlyGlob2(pattern), name) || globMatches(starOnlyGlob2(pattern), resolved.identity.table)))
|
|
52300
52628
|
continue;
|
|
52301
52629
|
if (ignored.has(key))
|
|
52302
52630
|
continue;
|
|
@@ -52474,10 +52802,6 @@ function unparsedGaps(schema, side) {
|
|
|
52474
52802
|
reason: entry.reason
|
|
52475
52803
|
}));
|
|
52476
52804
|
}
|
|
52477
|
-
function globToRegex3(glob) {
|
|
52478
|
-
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
52479
|
-
return new RegExp(`^${escaped}$`);
|
|
52480
|
-
}
|
|
52481
52805
|
function gapOrder(left, right) {
|
|
52482
52806
|
return codePointOrder5(left.side, right.side) || codePointOrder5(left.kind, right.kind) || codePointOrder5(left.location, right.location) || codePointOrder5(left.reason, right.reason);
|
|
52483
52807
|
}
|
|
@@ -52493,6 +52817,7 @@ function codePointOrder5(left, right) {
|
|
|
52493
52817
|
}
|
|
52494
52818
|
var DEFAULT_IGNORE2, NormalizedChangeSetError;
|
|
52495
52819
|
var init_change_set = __esm(() => {
|
|
52820
|
+
init_glob();
|
|
52496
52821
|
DEFAULT_IGNORE2 = ["_prisma_migrations"];
|
|
52497
52822
|
NormalizedChangeSetError = class NormalizedChangeSetError extends Error {
|
|
52498
52823
|
constructor(message) {
|