taurusdb-core 0.1.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/README.md +21 -0
- package/dist/auth/secret-resolver.d.ts +16 -0
- package/dist/auth/secret-resolver.js +64 -0
- package/dist/auth/sql-profile-loader/env-source.d.ts +3 -0
- package/dist/auth/sql-profile-loader/env-source.js +94 -0
- package/dist/auth/sql-profile-loader/file-source.d.ts +6 -0
- package/dist/auth/sql-profile-loader/file-source.js +40 -0
- package/dist/auth/sql-profile-loader/loader.d.ts +16 -0
- package/dist/auth/sql-profile-loader/loader.js +81 -0
- package/dist/auth/sql-profile-loader/parsing.d.ts +14 -0
- package/dist/auth/sql-profile-loader/parsing.js +216 -0
- package/dist/auth/sql-profile-loader/runtime-override.d.ts +14 -0
- package/dist/auth/sql-profile-loader/runtime-override.js +52 -0
- package/dist/auth/sql-profile-loader/types.d.ts +64 -0
- package/dist/auth/sql-profile-loader/types.js +1 -0
- package/dist/auth/sql-profile-loader.d.ts +4 -0
- package/dist/auth/sql-profile-loader.js +3 -0
- package/dist/capability/feature-matrix.d.ts +5 -0
- package/dist/capability/feature-matrix.js +237 -0
- package/dist/capability/probe.d.ts +19 -0
- package/dist/capability/probe.js +139 -0
- package/dist/capability/types.d.ts +49 -0
- package/dist/capability/types.js +16 -0
- package/dist/capability/version.d.ts +3 -0
- package/dist/capability/version.js +47 -0
- package/dist/cloud/auth.d.ts +26 -0
- package/dist/cloud/auth.js +198 -0
- package/dist/cloud/instances.d.ts +46 -0
- package/dist/cloud/instances.js +224 -0
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.js +194 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +21 -0
- package/dist/config/redaction.d.ts +2 -0
- package/dist/config/redaction.js +19 -0
- package/dist/config/schema.d.ts +417 -0
- package/dist/config/schema.js +100 -0
- package/dist/context/datasource-resolver.d.ts +19 -0
- package/dist/context/datasource-resolver.js +71 -0
- package/dist/context/session-context.d.ts +26 -0
- package/dist/context/session-context.js +1 -0
- package/dist/diagnostics/metrics-source.d.ts +65 -0
- package/dist/diagnostics/metrics-source.js +280 -0
- package/dist/diagnostics/slow-sql-source/das-source.d.ts +43 -0
- package/dist/diagnostics/slow-sql-source/das-source.js +170 -0
- package/dist/diagnostics/slow-sql-source/factory.d.ts +5 -0
- package/dist/diagnostics/slow-sql-source/factory.js +87 -0
- package/dist/diagnostics/slow-sql-source/parsers.d.ts +7 -0
- package/dist/diagnostics/slow-sql-source/parsers.js +125 -0
- package/dist/diagnostics/slow-sql-source/taurus-api-source.d.ts +42 -0
- package/dist/diagnostics/slow-sql-source/taurus-api-source.js +149 -0
- package/dist/diagnostics/slow-sql-source/types.d.ts +40 -0
- package/dist/diagnostics/slow-sql-source/types.js +1 -0
- package/dist/diagnostics/slow-sql-source/utils.d.ts +20 -0
- package/dist/diagnostics/slow-sql-source/utils.js +170 -0
- package/dist/diagnostics/slow-sql-source.d.ts +4 -0
- package/dist/diagnostics/slow-sql-source.js +3 -0
- package/dist/diagnostics/types.d.ts +189 -0
- package/dist/diagnostics/types.js +39 -0
- package/dist/engine/data-access/locks.d.ts +8 -0
- package/dist/engine/data-access/locks.js +146 -0
- package/dist/engine/data-access/processlist.d.ts +4 -0
- package/dist/engine/data-access/processlist.js +56 -0
- package/dist/engine/data-access/statements.d.ts +10 -0
- package/dist/engine/data-access/statements.js +203 -0
- package/dist/engine/data-access/storage.d.ts +6 -0
- package/dist/engine/data-access/storage.js +96 -0
- package/dist/engine/data-access.d.ts +4 -0
- package/dist/engine/data-access.js +4 -0
- package/dist/engine/diagnostics.d.ts +7 -0
- package/dist/engine/diagnostics.js +7 -0
- package/dist/engine/helper-modules/diagnostics.d.ts +57 -0
- package/dist/engine/helper-modules/diagnostics.js +322 -0
- package/dist/engine/helper-modules/parsers.d.ts +13 -0
- package/dist/engine/helper-modules/parsers.js +283 -0
- package/dist/engine/helper-modules/sql.d.ts +12 -0
- package/dist/engine/helper-modules/sql.js +119 -0
- package/dist/engine/helper-modules/types.d.ts +103 -0
- package/dist/engine/helper-modules/types.js +1 -0
- package/dist/engine/helpers.d.ts +4 -0
- package/dist/engine/helpers.js +4 -0
- package/dist/engine/runtime.d.ts +20 -0
- package/dist/engine/runtime.js +385 -0
- package/dist/engine/types.d.ts +125 -0
- package/dist/engine/types.js +1 -0
- package/dist/engine/workflows/connection-spike.d.ts +4 -0
- package/dist/engine/workflows/connection-spike.js +316 -0
- package/dist/engine/workflows/db-hotspot.d.ts +4 -0
- package/dist/engine/workflows/db-hotspot.js +182 -0
- package/dist/engine/workflows/lock-contention-helpers/entities.d.ts +9 -0
- package/dist/engine/workflows/lock-contention-helpers/entities.js +58 -0
- package/dist/engine/workflows/lock-contention-helpers/no-match.d.ts +3 -0
- package/dist/engine/workflows/lock-contention-helpers/no-match.js +65 -0
- package/dist/engine/workflows/lock-contention-helpers/report.d.ts +21 -0
- package/dist/engine/workflows/lock-contention-helpers/report.js +104 -0
- package/dist/engine/workflows/lock-contention-helpers/root-cause.d.ts +4 -0
- package/dist/engine/workflows/lock-contention-helpers/root-cause.js +79 -0
- package/dist/engine/workflows/lock-contention-helpers/signals.d.ts +22 -0
- package/dist/engine/workflows/lock-contention-helpers/signals.js +34 -0
- package/dist/engine/workflows/lock-contention-helpers.d.ts +5 -0
- package/dist/engine/workflows/lock-contention-helpers.js +5 -0
- package/dist/engine/workflows/lock-contention.d.ts +4 -0
- package/dist/engine/workflows/lock-contention.js +67 -0
- package/dist/engine/workflows/service-latency.d.ts +4 -0
- package/dist/engine/workflows/service-latency.js +262 -0
- package/dist/engine/workflows/slow-query-helpers.d.ts +41 -0
- package/dist/engine/workflows/slow-query-helpers.js +253 -0
- package/dist/engine/workflows/slow-query.d.ts +4 -0
- package/dist/engine/workflows/slow-query.js +156 -0
- package/dist/engine/workflows/storage-pressure-helpers.d.ts +12 -0
- package/dist/engine/workflows/storage-pressure-helpers.js +281 -0
- package/dist/engine/workflows/storage-pressure.d.ts +4 -0
- package/dist/engine/workflows/storage-pressure.js +27 -0
- package/dist/engine/workflows/top-slow-sql.d.ts +4 -0
- package/dist/engine/workflows/top-slow-sql.js +222 -0
- package/dist/engine.d.ts +77 -0
- package/dist/engine.js +240 -0
- package/dist/executor/adapters/mysql.d.ts +2 -0
- package/dist/executor/adapters/mysql.js +114 -0
- package/dist/executor/connection-pool.d.ts +105 -0
- package/dist/executor/connection-pool.js +236 -0
- package/dist/executor/explain.d.ts +5 -0
- package/dist/executor/explain.js +119 -0
- package/dist/executor/query-tracker.d.ts +45 -0
- package/dist/executor/query-tracker.js +83 -0
- package/dist/executor/result-normalizer.d.ts +6 -0
- package/dist/executor/result-normalizer.js +47 -0
- package/dist/executor/sql-executor.d.ts +32 -0
- package/dist/executor/sql-executor.js +250 -0
- package/dist/executor/types.d.ts +70 -0
- package/dist/executor/types.js +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +21 -0
- package/dist/safety/confirmation-store.d.ts +44 -0
- package/dist/safety/confirmation-store.js +130 -0
- package/dist/safety/guardrail.d.ts +39 -0
- package/dist/safety/guardrail.js +99 -0
- package/dist/safety/parser/adapter.d.ts +10 -0
- package/dist/safety/parser/adapter.js +72 -0
- package/dist/safety/parser/ast-utils.d.ts +10 -0
- package/dist/safety/parser/ast-utils.js +167 -0
- package/dist/safety/parser/features.d.ts +12 -0
- package/dist/safety/parser/features.js +113 -0
- package/dist/safety/parser/index.d.ts +2 -0
- package/dist/safety/parser/index.js +1 -0
- package/dist/safety/parser/types.d.ts +76 -0
- package/dist/safety/parser/types.js +1 -0
- package/dist/safety/redaction.d.ts +34 -0
- package/dist/safety/redaction.js +186 -0
- package/dist/safety/sql-classifier.d.ts +19 -0
- package/dist/safety/sql-classifier.js +43 -0
- package/dist/safety/sql-validator.d.ts +19 -0
- package/dist/safety/sql-validator.js +143 -0
- package/dist/schema/adapters/mysql.d.ts +16 -0
- package/dist/schema/adapters/mysql.js +287 -0
- package/dist/schema/introspector.d.ts +70 -0
- package/dist/schema/introspector.js +40 -0
- package/dist/taurus/flashback.d.ts +36 -0
- package/dist/taurus/flashback.js +149 -0
- package/dist/taurus/recycle-bin.d.ts +14 -0
- package/dist/taurus/recycle-bin.js +61 -0
- package/dist/utils/formatter.d.ts +70 -0
- package/dist/utils/formatter.js +60 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +247 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.js +11 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +46 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getFunctionName, isObject, toLimitNode, toWhereNode } from "./ast-utils.js";
|
|
2
|
+
const AGGREGATE_FUNCTIONS = new Set([
|
|
3
|
+
"COUNT",
|
|
4
|
+
"SUM",
|
|
5
|
+
"AVG",
|
|
6
|
+
"MIN",
|
|
7
|
+
"MAX",
|
|
8
|
+
"GROUP_CONCAT",
|
|
9
|
+
"STRING_AGG",
|
|
10
|
+
"JSON_AGG",
|
|
11
|
+
"ARRAY_AGG",
|
|
12
|
+
]);
|
|
13
|
+
export function scanAst(statements) {
|
|
14
|
+
let hasAggregate = false;
|
|
15
|
+
let hasSubquery = false;
|
|
16
|
+
const visit = (node, isTopLevelStatement, parentType, parentKey) => {
|
|
17
|
+
if (Array.isArray(node)) {
|
|
18
|
+
for (const entry of node) {
|
|
19
|
+
visit(entry, false, parentType, parentKey);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!isObject(node)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const nodeType = typeof node.type === "string" ? node.type.toLowerCase() : undefined;
|
|
27
|
+
if (nodeType === "aggr_func") {
|
|
28
|
+
hasAggregate = true;
|
|
29
|
+
}
|
|
30
|
+
else if (nodeType === "function") {
|
|
31
|
+
const fn = getFunctionName(node.name);
|
|
32
|
+
if (fn && AGGREGATE_FUNCTIONS.has(fn.toUpperCase())) {
|
|
33
|
+
hasAggregate = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (nodeType === "select" && !isTopLevelStatement) {
|
|
37
|
+
const isExplainPayload = parentType === "explain" && parentKey === "expr";
|
|
38
|
+
if (!isExplainPayload) {
|
|
39
|
+
hasSubquery = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const currentType = typeof node.type === "string" ? node.type.toLowerCase() : undefined;
|
|
43
|
+
for (const [key, value] of Object.entries(node)) {
|
|
44
|
+
visit(value, false, currentType, key);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for (const statement of statements) {
|
|
48
|
+
visit(statement, true);
|
|
49
|
+
}
|
|
50
|
+
return { hasAggregate, hasSubquery };
|
|
51
|
+
}
|
|
52
|
+
export function collectStructuredFeatures(statements) {
|
|
53
|
+
let where;
|
|
54
|
+
let limit;
|
|
55
|
+
const joins = [];
|
|
56
|
+
const orderBy = [];
|
|
57
|
+
const groupBy = [];
|
|
58
|
+
const roots = [];
|
|
59
|
+
for (const statement of statements) {
|
|
60
|
+
roots.push(statement);
|
|
61
|
+
const statementType = typeof statement.type === "string" ? statement.type.toLowerCase() : "";
|
|
62
|
+
if (statementType === "explain" && isObject(statement.expr)) {
|
|
63
|
+
roots.push(statement.expr);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const node of roots) {
|
|
67
|
+
if (!where && node.where !== null && node.where !== undefined) {
|
|
68
|
+
where = toWhereNode(node.where);
|
|
69
|
+
}
|
|
70
|
+
if (!limit && node.limit !== null && node.limit !== undefined) {
|
|
71
|
+
limit = toLimitNode(node.limit);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(node.from)) {
|
|
74
|
+
for (const fromEntry of node.from) {
|
|
75
|
+
if (!isObject(fromEntry)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const joinType = typeof fromEntry.join === "string" ? fromEntry.join : undefined;
|
|
79
|
+
if (!joinType) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const tableName = typeof fromEntry.table === "string" ? fromEntry.table : undefined;
|
|
83
|
+
const schemaName = typeof fromEntry.db === "string" ? fromEntry.db : undefined;
|
|
84
|
+
joins.push({
|
|
85
|
+
type: joinType,
|
|
86
|
+
table: tableName ? { name: tableName, schema: schemaName ?? undefined } : undefined,
|
|
87
|
+
hasOn: fromEntry.on !== undefined && fromEntry.on !== null,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(node.orderby)) {
|
|
92
|
+
for (const orderItem of node.orderby) {
|
|
93
|
+
let direction;
|
|
94
|
+
if (isObject(orderItem) && typeof orderItem.type === "string") {
|
|
95
|
+
direction = orderItem.type.toUpperCase();
|
|
96
|
+
}
|
|
97
|
+
orderBy.push({
|
|
98
|
+
direction,
|
|
99
|
+
raw: orderItem,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (isObject(node.groupby) && Array.isArray(node.groupby.columns)) {
|
|
104
|
+
for (const groupItem of node.groupby.columns) {
|
|
105
|
+
groupBy.push({ raw: groupItem });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (node.groupby !== null && node.groupby !== undefined) {
|
|
109
|
+
groupBy.push({ raw: node.groupby });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { where, limit, joins, orderBy, groupBy };
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NodeSqlParserAdapter, createSqlParser } from "./adapter.js";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type AstNode = Record<string, unknown>;
|
|
2
|
+
export type StatementType = "select" | "show" | "explain" | "describe" | "insert" | "update" | "delete" | "alter" | "drop" | "create" | "grant" | "revoke" | "truncate" | "set" | "use" | "unknown";
|
|
3
|
+
export interface TableRef {
|
|
4
|
+
name: string;
|
|
5
|
+
schema?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ColumnRef {
|
|
8
|
+
name: string;
|
|
9
|
+
table?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface WhereNode {
|
|
12
|
+
kind: "binary" | "expression";
|
|
13
|
+
raw: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface LimitNode {
|
|
16
|
+
raw: unknown;
|
|
17
|
+
rowCount?: number;
|
|
18
|
+
offset?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface JoinNode {
|
|
21
|
+
type?: string;
|
|
22
|
+
table?: TableRef;
|
|
23
|
+
hasOn?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface OrderByNode {
|
|
26
|
+
direction?: string;
|
|
27
|
+
raw: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface GroupByNode {
|
|
30
|
+
raw: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface SqlAst {
|
|
33
|
+
kind: StatementType;
|
|
34
|
+
tables: TableRef[];
|
|
35
|
+
columns: ColumnRef[];
|
|
36
|
+
where?: WhereNode;
|
|
37
|
+
limit?: LimitNode;
|
|
38
|
+
joins?: JoinNode[];
|
|
39
|
+
orderBy?: OrderByNode[];
|
|
40
|
+
groupBy?: GroupByNode[];
|
|
41
|
+
hasAggregate: boolean;
|
|
42
|
+
hasSubquery: boolean;
|
|
43
|
+
isMultiStatement: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface NormalizedSql {
|
|
46
|
+
normalizedSql: string;
|
|
47
|
+
sqlHash: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ParseError {
|
|
50
|
+
code: "SQL_PARSE_ERROR";
|
|
51
|
+
message: string;
|
|
52
|
+
position?: {
|
|
53
|
+
line: number;
|
|
54
|
+
column: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export type ParseResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
ast: SqlAst;
|
|
60
|
+
isMultiStatement: boolean;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
error: ParseError;
|
|
64
|
+
};
|
|
65
|
+
export interface SqlParser {
|
|
66
|
+
normalize(sql: string): NormalizedSql;
|
|
67
|
+
parse(sql: string): ParseResult;
|
|
68
|
+
}
|
|
69
|
+
export type ParserErrorLike = Error & {
|
|
70
|
+
location?: {
|
|
71
|
+
start?: {
|
|
72
|
+
line?: number;
|
|
73
|
+
column?: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type SensitiveStrategy = "mask" | "drop" | "hash";
|
|
2
|
+
export interface RedactionColumn {
|
|
3
|
+
name: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RawQueryResult {
|
|
7
|
+
columns: RedactionColumn[];
|
|
8
|
+
rows: unknown[][];
|
|
9
|
+
rowCount: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RedactionPolicy {
|
|
12
|
+
maxRows: number;
|
|
13
|
+
maxColumns: number;
|
|
14
|
+
maxFieldChars: number;
|
|
15
|
+
sensitiveColumns?: Iterable<string>;
|
|
16
|
+
sensitiveStrategy?: SensitiveStrategy;
|
|
17
|
+
}
|
|
18
|
+
export interface RedactedQueryResult {
|
|
19
|
+
columns: RedactionColumn[];
|
|
20
|
+
rows: unknown[][];
|
|
21
|
+
rowCount: number;
|
|
22
|
+
originalRowCount: number;
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
rowTruncated: boolean;
|
|
25
|
+
columnTruncated: boolean;
|
|
26
|
+
fieldTruncated: boolean;
|
|
27
|
+
redactedColumns: string[];
|
|
28
|
+
droppedColumns: string[];
|
|
29
|
+
truncatedColumns: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface ResultRedactor {
|
|
32
|
+
redact(raw: RawQueryResult, policy: RedactionPolicy): RedactedQueryResult;
|
|
33
|
+
}
|
|
34
|
+
export declare function createResultRedactor(): ResultRedactor;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const DEFAULT_MAX_ROWS = 200;
|
|
3
|
+
const DEFAULT_MAX_COLUMNS = 50;
|
|
4
|
+
const DEFAULT_MAX_FIELD_CHARS = 2048;
|
|
5
|
+
const DEFAULT_SENSITIVE_STRATEGY = "mask";
|
|
6
|
+
const SENSITIVE_COLUMN_PATTERNS = [
|
|
7
|
+
/password|passwd|secret/i,
|
|
8
|
+
/token|api_?key|access_?key|refresh_?token/i,
|
|
9
|
+
/phone|mobile|tel/i,
|
|
10
|
+
/email/i,
|
|
11
|
+
/id_?card|passport|ssn/i,
|
|
12
|
+
/bank|card_?no|account/i,
|
|
13
|
+
];
|
|
14
|
+
function asPositiveInt(value, fallback) {
|
|
15
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
function asNonNegativeInt(value, fallback) {
|
|
21
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
22
|
+
return Math.floor(value);
|
|
23
|
+
}
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
function normalizeColumnName(name) {
|
|
27
|
+
return name.trim().replace(/^[`'"]+|[`'"]+$/g, "").toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function columnCandidates(name) {
|
|
30
|
+
const normalized = normalizeColumnName(name);
|
|
31
|
+
const dotIndex = normalized.lastIndexOf(".");
|
|
32
|
+
if (dotIndex < 0) {
|
|
33
|
+
return [normalized];
|
|
34
|
+
}
|
|
35
|
+
return [normalized, normalized.slice(dotIndex + 1)];
|
|
36
|
+
}
|
|
37
|
+
function buildSensitiveSet(input) {
|
|
38
|
+
const set = new Set();
|
|
39
|
+
if (!input) {
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
for (const value of input) {
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const normalized = normalizeColumnName(value);
|
|
47
|
+
if (normalized) {
|
|
48
|
+
set.add(normalized);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return set;
|
|
52
|
+
}
|
|
53
|
+
function isSensitiveColumn(name, explicitSensitive) {
|
|
54
|
+
const candidates = columnCandidates(name);
|
|
55
|
+
if (candidates.some((candidate) => explicitSensitive.has(candidate))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const base = candidates[candidates.length - 1];
|
|
59
|
+
return SENSITIVE_COLUMN_PATTERNS.some((pattern) => pattern.test(base));
|
|
60
|
+
}
|
|
61
|
+
function maskValue(columnName, value) {
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
const baseName = columnCandidates(columnName).at(-1) ?? "";
|
|
66
|
+
const text = typeof value === "string" ? value : String(value);
|
|
67
|
+
if (/phone|mobile|tel/i.test(baseName)) {
|
|
68
|
+
return text.replace(/(\d{3})\d+(\d{4})/, "$1****$2");
|
|
69
|
+
}
|
|
70
|
+
if (/email/i.test(baseName)) {
|
|
71
|
+
return text.replace(/(.{2}).*(@.*)/, "$1***$2");
|
|
72
|
+
}
|
|
73
|
+
if (/id_?card|passport|ssn/i.test(baseName)) {
|
|
74
|
+
if (text.length <= 10) {
|
|
75
|
+
return "***";
|
|
76
|
+
}
|
|
77
|
+
return `${text.slice(0, 6)}********${text.slice(-4)}`;
|
|
78
|
+
}
|
|
79
|
+
return "***";
|
|
80
|
+
}
|
|
81
|
+
function hashValue(value) {
|
|
82
|
+
const plain = typeof value === "string"
|
|
83
|
+
? value
|
|
84
|
+
: value === null || value === undefined
|
|
85
|
+
? ""
|
|
86
|
+
: JSON.stringify(value);
|
|
87
|
+
const digest = createHash("sha256").update(plain).digest("hex").slice(0, 12);
|
|
88
|
+
return `[HASH:${digest}]`;
|
|
89
|
+
}
|
|
90
|
+
function truncateFieldValue(value, maxFieldChars) {
|
|
91
|
+
if (typeof value !== "string") {
|
|
92
|
+
return { value, truncated: false };
|
|
93
|
+
}
|
|
94
|
+
if (value.length <= maxFieldChars) {
|
|
95
|
+
return { value, truncated: false };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
value: `${value.slice(0, maxFieldChars)}...[TRUNCATED]`,
|
|
99
|
+
truncated: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
class DefaultResultRedactor {
|
|
103
|
+
redact(raw, policy) {
|
|
104
|
+
const maxRows = asPositiveInt(policy.maxRows, DEFAULT_MAX_ROWS);
|
|
105
|
+
const maxColumns = asPositiveInt(policy.maxColumns, DEFAULT_MAX_COLUMNS);
|
|
106
|
+
const maxFieldChars = asPositiveInt(policy.maxFieldChars, DEFAULT_MAX_FIELD_CHARS);
|
|
107
|
+
const sensitiveStrategy = policy.sensitiveStrategy ?? DEFAULT_SENSITIVE_STRATEGY;
|
|
108
|
+
const explicitSensitive = buildSensitiveSet(policy.sensitiveColumns);
|
|
109
|
+
const sourceColumns = Array.isArray(raw.columns) ? raw.columns : [];
|
|
110
|
+
const sourceRows = Array.isArray(raw.rows) ? raw.rows : [];
|
|
111
|
+
const originalRowCount = asNonNegativeInt(raw.rowCount, sourceRows.length);
|
|
112
|
+
const rowTruncated = sourceRows.length > maxRows || originalRowCount > maxRows;
|
|
113
|
+
const columnTruncated = sourceColumns.length > maxColumns;
|
|
114
|
+
const rowLimited = sourceRows.slice(0, maxRows);
|
|
115
|
+
const columnLimited = sourceColumns.slice(0, maxColumns);
|
|
116
|
+
const keepColumnIndexes = [];
|
|
117
|
+
const keepColumns = [];
|
|
118
|
+
const keepColumnSensitiveFlags = [];
|
|
119
|
+
const redactedColumns = new Set();
|
|
120
|
+
const droppedColumns = [];
|
|
121
|
+
for (let index = 0; index < columnLimited.length; index += 1) {
|
|
122
|
+
const column = columnLimited[index];
|
|
123
|
+
const sensitive = isSensitiveColumn(column.name, explicitSensitive);
|
|
124
|
+
if (sensitive && sensitiveStrategy === "drop") {
|
|
125
|
+
droppedColumns.push(column.name);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
keepColumnIndexes.push(index);
|
|
129
|
+
keepColumns.push(column);
|
|
130
|
+
keepColumnSensitiveFlags.push(sensitive);
|
|
131
|
+
if (sensitive) {
|
|
132
|
+
redactedColumns.add(column.name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const truncatedColumns = new Set();
|
|
136
|
+
const outputRows = rowLimited.map((row) => {
|
|
137
|
+
const sourceRow = Array.isArray(row) ? row : [row];
|
|
138
|
+
const outputRow = [];
|
|
139
|
+
for (let outputIndex = 0; outputIndex < keepColumns.length; outputIndex += 1) {
|
|
140
|
+
const sourceIndex = keepColumnIndexes[outputIndex];
|
|
141
|
+
const column = keepColumns[outputIndex];
|
|
142
|
+
const isSensitive = keepColumnSensitiveFlags[outputIndex];
|
|
143
|
+
const rawValue = sourceRow[sourceIndex];
|
|
144
|
+
let value;
|
|
145
|
+
if (isSensitive) {
|
|
146
|
+
if (sensitiveStrategy === "hash") {
|
|
147
|
+
value = hashValue(rawValue);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
value = maskValue(column.name, rawValue);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const truncated = truncateFieldValue(rawValue, maxFieldChars);
|
|
155
|
+
value = truncated.value;
|
|
156
|
+
if (truncated.truncated) {
|
|
157
|
+
truncatedColumns.add(column.name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
outputRow.push(value);
|
|
161
|
+
}
|
|
162
|
+
return outputRow;
|
|
163
|
+
});
|
|
164
|
+
const fieldTruncated = truncatedColumns.size > 0;
|
|
165
|
+
return {
|
|
166
|
+
columns: keepColumns,
|
|
167
|
+
rows: outputRows,
|
|
168
|
+
rowCount: originalRowCount,
|
|
169
|
+
originalRowCount,
|
|
170
|
+
truncated: rowTruncated || columnTruncated || fieldTruncated,
|
|
171
|
+
rowTruncated,
|
|
172
|
+
columnTruncated,
|
|
173
|
+
fieldTruncated,
|
|
174
|
+
redactedColumns: keepColumns
|
|
175
|
+
.map((column) => column.name)
|
|
176
|
+
.filter((name) => redactedColumns.has(name)),
|
|
177
|
+
droppedColumns,
|
|
178
|
+
truncatedColumns: keepColumns
|
|
179
|
+
.map((column) => column.name)
|
|
180
|
+
.filter((name) => truncatedColumns.has(name)),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function createResultRedactor() {
|
|
185
|
+
return new DefaultResultRedactor();
|
|
186
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DatabaseEngine } from "../auth/sql-profile-loader.js";
|
|
2
|
+
import type { NormalizedSql, SqlAst, StatementType } from "./parser/index.js";
|
|
3
|
+
export type GuardrailEngine = DatabaseEngine | "unknown";
|
|
4
|
+
export interface SqlClassification {
|
|
5
|
+
engine: GuardrailEngine;
|
|
6
|
+
statementType: StatementType;
|
|
7
|
+
normalizedSql: string;
|
|
8
|
+
sqlHash: string;
|
|
9
|
+
isMultiStatement: boolean;
|
|
10
|
+
referencedTables: string[];
|
|
11
|
+
referencedColumns: string[];
|
|
12
|
+
hasWhere: boolean;
|
|
13
|
+
hasLimit: boolean;
|
|
14
|
+
hasJoin: boolean;
|
|
15
|
+
hasSubquery: boolean;
|
|
16
|
+
hasOrderBy: boolean;
|
|
17
|
+
hasAggregate: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function classifySql(ast: SqlAst, normalized: NormalizedSql, engine: GuardrailEngine): SqlClassification;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function dedupeCaseInsensitive(values) {
|
|
2
|
+
const output = [];
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
for (const value of values) {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const key = trimmed.toLowerCase();
|
|
10
|
+
if (seen.has(key)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
seen.add(key);
|
|
14
|
+
output.push(trimmed);
|
|
15
|
+
}
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
function extractTables(ast) {
|
|
19
|
+
return dedupeCaseInsensitive(ast.tables.map((table) => (table.schema ? `${table.schema}.${table.name}` : table.name)));
|
|
20
|
+
}
|
|
21
|
+
function extractColumns(ast) {
|
|
22
|
+
return dedupeCaseInsensitive(ast.columns.map((column) => (column.table ? `${column.table}.${column.name}` : column.name)));
|
|
23
|
+
}
|
|
24
|
+
function extractStatementType(ast) {
|
|
25
|
+
return ast.kind;
|
|
26
|
+
}
|
|
27
|
+
export function classifySql(ast, normalized, engine) {
|
|
28
|
+
return {
|
|
29
|
+
engine,
|
|
30
|
+
statementType: extractStatementType(ast),
|
|
31
|
+
normalizedSql: normalized.normalizedSql,
|
|
32
|
+
sqlHash: normalized.sqlHash,
|
|
33
|
+
isMultiStatement: ast.isMultiStatement,
|
|
34
|
+
referencedTables: extractTables(ast),
|
|
35
|
+
referencedColumns: extractColumns(ast),
|
|
36
|
+
hasWhere: ast.where !== undefined,
|
|
37
|
+
hasLimit: ast.limit !== undefined,
|
|
38
|
+
hasJoin: (ast.joins?.length ?? 0) > 0,
|
|
39
|
+
hasSubquery: ast.hasSubquery,
|
|
40
|
+
hasOrderBy: (ast.orderBy?.length ?? 0) > 0,
|
|
41
|
+
hasAggregate: ast.hasAggregate,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SqlClassification } from "./sql-classifier.js";
|
|
2
|
+
export type RiskLevel = "low" | "medium" | "high" | "blocked";
|
|
3
|
+
export type ValidationAction = "allow" | "confirm" | "block";
|
|
4
|
+
export interface ValidationResult {
|
|
5
|
+
action: ValidationAction;
|
|
6
|
+
riskLevel: RiskLevel;
|
|
7
|
+
reasonCodes: string[];
|
|
8
|
+
riskHints: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ExplainRiskSummary {
|
|
11
|
+
fullTableScanLikely: boolean;
|
|
12
|
+
indexHitLikely: boolean;
|
|
13
|
+
estimatedRows: number | null;
|
|
14
|
+
usesTempStructure: boolean;
|
|
15
|
+
usesFilesort: boolean;
|
|
16
|
+
riskHints: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function validateToolScope(toolName: string, cls: SqlClassification): ValidationResult;
|
|
19
|
+
export declare function validateStaticRules(cls: SqlClassification): ValidationResult;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const READONLY_STATEMENTS = new Set(["select", "show", "explain", "describe"]);
|
|
2
|
+
const MUTATION_STATEMENTS = new Set(["insert", "update", "delete"]);
|
|
3
|
+
const STAR_COLUMN_PATTERN = /(^|\.)(\*|\(\.\*\))$/;
|
|
4
|
+
function allow(riskLevel = "low") {
|
|
5
|
+
return {
|
|
6
|
+
action: "allow",
|
|
7
|
+
riskLevel,
|
|
8
|
+
reasonCodes: [],
|
|
9
|
+
riskHints: [],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function confirm(riskLevel = "high", reasonCodes, riskHints) {
|
|
13
|
+
return {
|
|
14
|
+
action: "confirm",
|
|
15
|
+
riskLevel,
|
|
16
|
+
reasonCodes: dedupeCaseInsensitive(reasonCodes),
|
|
17
|
+
riskHints: dedupeCaseInsensitive(riskHints),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function block(reasonCodes, riskHints) {
|
|
21
|
+
return {
|
|
22
|
+
action: "block",
|
|
23
|
+
riskLevel: "blocked",
|
|
24
|
+
reasonCodes: dedupeCaseInsensitive(reasonCodes),
|
|
25
|
+
riskHints: dedupeCaseInsensitive(riskHints),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function dedupeCaseInsensitive(values) {
|
|
29
|
+
const output = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const raw of values) {
|
|
32
|
+
const value = raw.trim();
|
|
33
|
+
if (!value) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const key = value.toLowerCase();
|
|
37
|
+
if (seen.has(key)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
seen.add(key);
|
|
41
|
+
output.push(value);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
function containsSelectStar(cls) {
|
|
46
|
+
if (cls.statementType !== "select") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return cls.referencedColumns.some((column) => STAR_COLUMN_PATTERN.test(column.trim()));
|
|
50
|
+
}
|
|
51
|
+
export function validateToolScope(toolName, cls) {
|
|
52
|
+
if (toolName === "execute_readonly_sql") {
|
|
53
|
+
if (!READONLY_STATEMENTS.has(cls.statementType)) {
|
|
54
|
+
return block(["T001"], [
|
|
55
|
+
`Tool execute_readonly_sql only allows SELECT/SHOW/EXPLAIN/DESCRIBE, got ${cls.statementType}.`,
|
|
56
|
+
"Use execute_sql for controlled mutations.",
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
return allow("low");
|
|
60
|
+
}
|
|
61
|
+
if (toolName === "execute_sql") {
|
|
62
|
+
if (!MUTATION_STATEMENTS.has(cls.statementType)) {
|
|
63
|
+
return block(["T002"], [
|
|
64
|
+
`Tool execute_sql only allows INSERT/UPDATE/DELETE, got ${cls.statementType}.`,
|
|
65
|
+
"Use execute_readonly_sql for read statements.",
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
return allow("low");
|
|
69
|
+
}
|
|
70
|
+
if (toolName === "explain_sql") {
|
|
71
|
+
if (cls.statementType === "unknown") {
|
|
72
|
+
return block(["T003"], ["Tool explain_sql could not classify the statement type. Provide a single supported SQL statement."]);
|
|
73
|
+
}
|
|
74
|
+
return allow("low");
|
|
75
|
+
}
|
|
76
|
+
return allow("low");
|
|
77
|
+
}
|
|
78
|
+
export function validateStaticRules(cls) {
|
|
79
|
+
const reasonCodes = [];
|
|
80
|
+
const riskHints = [];
|
|
81
|
+
let hasBlock = false;
|
|
82
|
+
let hasConfirm = false;
|
|
83
|
+
let hasMedium = false;
|
|
84
|
+
const escalateToBlock = (code, hint) => {
|
|
85
|
+
hasBlock = true;
|
|
86
|
+
reasonCodes.push(code);
|
|
87
|
+
riskHints.push(hint);
|
|
88
|
+
};
|
|
89
|
+
const escalateToConfirm = (code, hint) => {
|
|
90
|
+
hasConfirm = true;
|
|
91
|
+
reasonCodes.push(code);
|
|
92
|
+
riskHints.push(hint);
|
|
93
|
+
};
|
|
94
|
+
const escalateToMediumAllow = (code, hint) => {
|
|
95
|
+
hasMedium = true;
|
|
96
|
+
reasonCodes.push(code);
|
|
97
|
+
riskHints.push(hint);
|
|
98
|
+
};
|
|
99
|
+
if (cls.isMultiStatement) {
|
|
100
|
+
escalateToBlock("R001", "Multi-statement SQL is blocked.");
|
|
101
|
+
}
|
|
102
|
+
if (cls.statementType === "grant" || cls.statementType === "revoke") {
|
|
103
|
+
escalateToBlock("R002", "DCL statements (GRANT/REVOKE) are blocked.");
|
|
104
|
+
}
|
|
105
|
+
if (cls.statementType === "truncate" ||
|
|
106
|
+
(cls.statementType === "drop" && /^DROP\s+DATABASE\b/i.test(cls.normalizedSql))) {
|
|
107
|
+
escalateToBlock("R003", "DROP DATABASE and TRUNCATE are blocked.");
|
|
108
|
+
}
|
|
109
|
+
if (cls.statementType === "set" && /^SET\s+GLOBAL\b/i.test(cls.normalizedSql)) {
|
|
110
|
+
escalateToBlock("R004", "SET GLOBAL is blocked.");
|
|
111
|
+
}
|
|
112
|
+
if (cls.statementType === "update" || cls.statementType === "delete") {
|
|
113
|
+
if (!cls.hasWhere) {
|
|
114
|
+
escalateToBlock("R005", "UPDATE/DELETE without WHERE is blocked.");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
escalateToConfirm("R006", "Mutation SQL with WHERE requires confirmation.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (cls.statementType === "select") {
|
|
121
|
+
if (!cls.hasLimit && !cls.hasAggregate) {
|
|
122
|
+
escalateToMediumAllow("R007", "Detail SELECT without LIMIT has medium risk.");
|
|
123
|
+
}
|
|
124
|
+
if (containsSelectStar(cls)) {
|
|
125
|
+
escalateToMediumAllow("R008", "SELECT * may return excessive columns.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (hasBlock) {
|
|
129
|
+
return block(reasonCodes, riskHints);
|
|
130
|
+
}
|
|
131
|
+
if (hasConfirm) {
|
|
132
|
+
return confirm("high", reasonCodes, riskHints);
|
|
133
|
+
}
|
|
134
|
+
if (hasMedium) {
|
|
135
|
+
return {
|
|
136
|
+
action: "allow",
|
|
137
|
+
riskLevel: "medium",
|
|
138
|
+
reasonCodes: dedupeCaseInsensitive(reasonCodes),
|
|
139
|
+
riskHints: dedupeCaseInsensitive(riskHints),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return allow("low");
|
|
143
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConnectionPool } from "../../executor/connection-pool.js";
|
|
2
|
+
import type { SessionContext } from "../../context/session-context.js";
|
|
3
|
+
import type { DatabaseInfo, SchemaAdapter, TableInfo, TableSchema } from "../introspector.js";
|
|
4
|
+
export type MySqlSchemaAdapterOptions = {
|
|
5
|
+
connectionPool: ConnectionPool;
|
|
6
|
+
};
|
|
7
|
+
export declare class MySqlSchemaAdapter implements SchemaAdapter {
|
|
8
|
+
private readonly connectionPool;
|
|
9
|
+
constructor(options: MySqlSchemaAdapterOptions);
|
|
10
|
+
listDatabases(ctx: SessionContext): Promise<DatabaseInfo[]>;
|
|
11
|
+
listTables(ctx: SessionContext, database: string): Promise<TableInfo[]>;
|
|
12
|
+
describeTable(ctx: SessionContext, database: string, table: string): Promise<TableSchema>;
|
|
13
|
+
private mapIndexes;
|
|
14
|
+
private queryObjects;
|
|
15
|
+
}
|
|
16
|
+
export declare function createMySqlSchemaAdapter(options: MySqlSchemaAdapterOptions): SchemaAdapter;
|