peekinside 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +45 -2
- package/bin/peekinside.js +8 -0
- package/index.d.ts +389 -0
- package/index.js +3 -0
- package/package.json +77 -4
- package/src/PeekInside.js +731 -0
- package/src/capture/body-capture.js +86 -0
- package/src/capture/body-decoder.js +188 -0
- package/src/capture/exchange-recorder.js +263 -0
- package/src/capture/http-message.js +138 -0
- package/src/capture/ignored-paths.js +49 -0
- package/src/capture/request-body-tap.js +40 -0
- package/src/capture/response-tap.js +83 -0
- package/src/cli.js +254 -0
- package/src/config.js +183 -0
- package/src/crypto.js +189 -0
- package/src/deployment.js +183 -0
- package/src/exceptions/exception-builder.js +339 -0
- package/src/exceptions/exception-capture.js +407 -0
- package/src/exceptions/exception-record.js +118 -0
- package/src/exceptions/fingerprint.js +60 -0
- package/src/exceptions/frame-classifier.js +114 -0
- package/src/exceptions/limits.js +95 -0
- package/src/exceptions/process-monitor.js +192 -0
- package/src/exceptions/rate-limiter.js +50 -0
- package/src/exceptions/request-context.js +337 -0
- package/src/exceptions/safe-json.js +186 -0
- package/src/exceptions/source-context.js +144 -0
- package/src/exceptions/source-maps.js +313 -0
- package/src/exceptions/stack-parser.js +179 -0
- package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
- package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
- package/src/hub/client.js +184 -0
- package/src/hub/entry.js +4 -0
- package/src/hub/exception-state.js +97 -0
- package/src/hub/ingest-payload.js +362 -0
- package/src/hub/process.js +175 -0
- package/src/hub/project-log.js +559 -0
- package/src/hub/segment-log.js +282 -0
- package/src/hub/server.js +538 -0
- package/src/hub/spool-ingester.js +110 -0
- package/src/hub/store.js +404 -0
- package/src/instrumentation/console.js +83 -0
- package/src/instrumentation/http.js +146 -0
- package/src/instrumentation/index.js +234 -0
- package/src/instrumentation/jobs.js +186 -0
- package/src/instrumentation/loggers.js +135 -0
- package/src/instrumentation/mongodb.js +221 -0
- package/src/instrumentation/mysql2.js +161 -0
- package/src/instrumentation/operation.js +70 -0
- package/src/instrumentation/patch.js +72 -0
- package/src/instrumentation/pg.js +218 -0
- package/src/instrumentation/prisma.js +181 -0
- package/src/integrations/express-errors.js +135 -0
- package/src/integrations/express.js +57 -0
- package/src/integrations/fastify.js +72 -0
- package/src/integrations/framework-adapter.js +53 -0
- package/src/integrations/http-server.js +32 -0
- package/src/integrations/koa.js +72 -0
- package/src/integrations/server-binder.js +120 -0
- package/src/jobs/job-context.js +40 -0
- package/src/jobs/job-run-record.js +104 -0
- package/src/jobs/job-runner.js +173 -0
- package/src/logs/log-capture.js +143 -0
- package/src/logs/log-record.js +74 -0
- package/src/project.js +76 -0
- package/src/record.js +181 -0
- package/src/release.js +157 -0
- package/src/runtime/loop-monitor.js +162 -0
- package/src/runtime/sampler.js +93 -0
- package/src/spool.js +195 -0
- package/src/timeline/caller.js +129 -0
- package/src/timeline/execution-hash.js +110 -0
- package/src/timeline/lru.js +50 -0
- package/src/timeline/masking.js +266 -0
- package/src/timeline/shape.js +77 -0
- package/src/timeline/sql.js +396 -0
- package/src/timeline/timeline.js +642 -0
- package/src/transport/batch.js +324 -0
- package/src/transport/cloud.js +363 -0
- package/src/transport/local.js +354 -0
- package/src/user.js +83 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads SQL statements the way the timeline needs them, without a full parser:
|
|
3
|
+
*
|
|
4
|
+
* - normalize: the statement without its values, so executions of the same statement group
|
|
5
|
+
* together (string literals, numbers and placeholders become `?`, `in (?, ?)` becomes `in (?)`,
|
|
6
|
+
* `values (?, ?), (?, ?)` becomes `values (?)`, whitespace is collapsed, words are lowercased,
|
|
7
|
+
* comments are dropped);
|
|
8
|
+
* - columns: which column each placeholder ($1 / ?) is bound to, so a value bound to a password
|
|
9
|
+
* column can be masked;
|
|
10
|
+
* - masked text: the statement with string literals compared to (or assigned to) sensitive columns
|
|
11
|
+
* replaced by '[masked]'.
|
|
12
|
+
*
|
|
13
|
+
* Everything works on a token stream that knows string literals, quoted identifiers, comments and
|
|
14
|
+
* dollar-quoted strings, so a `?` or `$1` inside a literal or a comment is never mistaken for a
|
|
15
|
+
* placeholder. Results are cached per statement text: an app sends the same few statements over
|
|
16
|
+
* and over.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
import { Masker } from './masking.js';
|
|
21
|
+
import { LruCache } from './lru.js';
|
|
22
|
+
|
|
23
|
+
// Statements longer than this are analyzed on their start only: the timeline keeps 4 KB of text,
|
|
24
|
+
// and a bulk insert of megabytes must not cost megabytes of tokenizing per execution.
|
|
25
|
+
const MAX_ANALYZED = 64 * 1024;
|
|
26
|
+
// Text kept per analysis (what the timeline shows), and the longest statement cached: longer ones
|
|
27
|
+
// are mostly one-offs (bulk inserts with inline values) that would only push the others out.
|
|
28
|
+
const MAX_KEPT = 4096;
|
|
29
|
+
const MAX_CACHED = 4096;
|
|
30
|
+
const COMPARISON = new Set(['=', '<>', '!=', '<', '>', '<=', '>=']);
|
|
31
|
+
const COMPARISON_WORDS = new Set(['like', 'ilike']);
|
|
32
|
+
const QUANTIFIERS = new Set(['any', 'all', 'some']);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} SqlAnalysis
|
|
36
|
+
* @property {string} fingerprint - 16 hex chars: sha256 of the normalized text (see normalize())
|
|
37
|
+
* @property {string} maskedText - The text with sensitive literals masked, at most 4096 characters
|
|
38
|
+
* @property {Map<number, string>} columns - Placeholder index (0-based, params[0] is $1 / the first
|
|
39
|
+
* ?) -> the column it is bound to
|
|
40
|
+
* @property {Set<number>} sensitive - Placeholder indexes bound to sensitive columns
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SQL analysis with a cache.
|
|
45
|
+
*/
|
|
46
|
+
export class SqlText {
|
|
47
|
+
/**
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {number} [options.cacheSize=500] - Distinct statements kept analyzed
|
|
50
|
+
*/
|
|
51
|
+
constructor({ cacheSize = 500 } = {}) {
|
|
52
|
+
this.cache = new LruCache(cacheSize);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Analyzes a statement, from the cache when it was seen before.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} text - SQL as the app sent it
|
|
59
|
+
* @param {'postgresql'|'mysql'|string} dialect - Decides what "..." means: an identifier in
|
|
60
|
+
* PostgreSQL, a string in MySQL
|
|
61
|
+
* @returns {SqlAnalysis}
|
|
62
|
+
*/
|
|
63
|
+
analyze(text, dialect) {
|
|
64
|
+
if (typeof text !== 'string' || text.length > MAX_CACHED) return SqlText.analyzeUncached(text, dialect);
|
|
65
|
+
const key = `${dialect}\u0000${text}`;
|
|
66
|
+
let analysis = this.cache.get(key);
|
|
67
|
+
if (!analysis) {
|
|
68
|
+
analysis = SqlText.analyzeUncached(text, dialect);
|
|
69
|
+
this.cache.set(key, analysis);
|
|
70
|
+
}
|
|
71
|
+
return analysis;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @param {string} dialect
|
|
77
|
+
* @returns {SqlAnalysis}
|
|
78
|
+
*/
|
|
79
|
+
static analyzeUncached(text, dialect) {
|
|
80
|
+
const source = typeof text === 'string' ? text.slice(0, MAX_ANALYZED) : String(text);
|
|
81
|
+
const tokens = tokenize(source, dialectOf(dialect));
|
|
82
|
+
const normalized = normalize(tokens);
|
|
83
|
+
const { columns, sensitive, literalColumns } = bindColumns(tokens);
|
|
84
|
+
return {
|
|
85
|
+
fingerprint: createHash('sha256').update(normalized).digest('hex').slice(0, 16),
|
|
86
|
+
maskedText: maskLiterals(source, tokens, literalColumns).slice(0, MAX_KEPT),
|
|
87
|
+
columns,
|
|
88
|
+
sensitive
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalizes a statement (see the module comment).
|
|
94
|
+
*
|
|
95
|
+
* @param {string} text
|
|
96
|
+
* @param {string} [dialect='postgresql']
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
static normalize(text, dialect = 'postgresql') {
|
|
100
|
+
return normalize(tokenize(String(text).slice(0, MAX_ANALYZED), dialectOf(dialect)));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// What the dialect changes in tokenizing: "..." is a string in MySQL and an identifier elsewhere;
|
|
105
|
+
// PostgreSQL numbers its placeholders ($1) and uses ? as an operator; others use ? placeholders.
|
|
106
|
+
function dialectOf(name) {
|
|
107
|
+
return { mysql: name === 'mysql', pg: name === 'postgresql' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Tokens ---------------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
// kind: space, comment, string, ident (quoted identifier), number, placeholder, word, punct.
|
|
113
|
+
// For placeholders, `index` is the 0-based parameter index.
|
|
114
|
+
function tokenize(text, { mysql, pg }) {
|
|
115
|
+
const tokens = [];
|
|
116
|
+
let i = 0;
|
|
117
|
+
let nextQuestion = 0;
|
|
118
|
+
const push = (kind, start, end, extra) => tokens.push({ kind, start, end, text: text.slice(start, end), ...extra });
|
|
119
|
+
|
|
120
|
+
while (i < text.length) {
|
|
121
|
+
const ch = text[i];
|
|
122
|
+
const next = text[i + 1];
|
|
123
|
+
const start = i;
|
|
124
|
+
|
|
125
|
+
if (/\s/.test(ch)) {
|
|
126
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
127
|
+
push('space', start, i);
|
|
128
|
+
} else if ((ch === '-' && next === '-') || (mysql && ch === '#')) {
|
|
129
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
130
|
+
push('comment', start, i);
|
|
131
|
+
} else if (ch === '/' && next === '*') {
|
|
132
|
+
const end = text.indexOf('*/', i + 2);
|
|
133
|
+
i = end === -1 ? text.length : end + 2;
|
|
134
|
+
push('comment', start, i);
|
|
135
|
+
} else if (ch === "'" || ((ch === 'E' || ch === 'e') && next === "'" && pg) || (mysql && ch === '"')) {
|
|
136
|
+
// 'it''s' always; backslash escapes in MySQL and in PostgreSQL E'' strings.
|
|
137
|
+
const quote = ch === '"' ? '"' : "'";
|
|
138
|
+
const backslash = mysql || ch === 'E' || ch === 'e';
|
|
139
|
+
i = ch === 'E' || ch === 'e' ? i + 2 : i + 1;
|
|
140
|
+
while (i < text.length) {
|
|
141
|
+
if (backslash && text[i] === '\\') {
|
|
142
|
+
i += 2;
|
|
143
|
+
} else if (text[i] === quote) {
|
|
144
|
+
if (text[i + 1] === quote) i += 2;
|
|
145
|
+
else {
|
|
146
|
+
i++;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
push('string', start, i);
|
|
154
|
+
} else if (ch === '"' || ch === '`') {
|
|
155
|
+
const end = closingQuote(text, i, ch);
|
|
156
|
+
i = end;
|
|
157
|
+
push('ident', start, i, { name: text.slice(start + 1, i - 1).split(ch + ch).join(ch) });
|
|
158
|
+
} else if (ch === '$' && pg && /[0-9]/.test(next || '')) {
|
|
159
|
+
i++;
|
|
160
|
+
while (i < text.length && /[0-9]/.test(text[i])) i++;
|
|
161
|
+
push('placeholder', start, i, { index: Number(text.slice(start + 1, i)) - 1 });
|
|
162
|
+
} else if (ch === '$' && pg && /[A-Za-z_$]/.test(next || '')) {
|
|
163
|
+
// Dollar-quoted string: $$...$$ or $tag$...$tag$.
|
|
164
|
+
const tagEnd = text.indexOf('$', i + 1);
|
|
165
|
+
const tag = tagEnd === -1 ? null : text.slice(i, tagEnd + 1);
|
|
166
|
+
if (tag && /^\$[A-Za-z_]*\$$/.test(tag)) {
|
|
167
|
+
const end = text.indexOf(tag, tagEnd + 1);
|
|
168
|
+
i = end === -1 ? text.length : end + tag.length;
|
|
169
|
+
push('string', start, i);
|
|
170
|
+
} else {
|
|
171
|
+
i++;
|
|
172
|
+
push('punct', start, i);
|
|
173
|
+
}
|
|
174
|
+
} else if (ch === '?' && !pg) {
|
|
175
|
+
// mysql2's ?? (an identifier) consumes a value too.
|
|
176
|
+
i += next === '?' ? 2 : 1;
|
|
177
|
+
push('placeholder', start, i, { index: nextQuestion++ });
|
|
178
|
+
} else if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(next || ''))) {
|
|
179
|
+
if (ch === '0' && (next === 'x' || next === 'X')) {
|
|
180
|
+
i += 2;
|
|
181
|
+
while (i < text.length && /[0-9a-fA-F]/.test(text[i])) i++;
|
|
182
|
+
} else {
|
|
183
|
+
while (i < text.length && /[0-9.]/.test(text[i])) i++;
|
|
184
|
+
if ((text[i] === 'e' || text[i] === 'E') && /[0-9+-]/.test(text[i + 1] || '')) {
|
|
185
|
+
i += 2;
|
|
186
|
+
while (i < text.length && /[0-9]/.test(text[i])) i++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
push('number', start, i);
|
|
190
|
+
} else if (/[A-Za-z_\u0080-]/.test(ch)) {
|
|
191
|
+
while (i < text.length && /[A-Za-z0-9_$\u0080-]/.test(text[i])) i++;
|
|
192
|
+
push('word', start, i);
|
|
193
|
+
} else {
|
|
194
|
+
// Two-character operators, then single characters.
|
|
195
|
+
const pair = ch + (next || '');
|
|
196
|
+
i += ['<>', '!=', '<=', '>=', '::', '||'].includes(pair) ? 2 : 1;
|
|
197
|
+
push('punct', start, i);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return tokens;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function closingQuote(text, open, quote) {
|
|
204
|
+
let i = open + 1;
|
|
205
|
+
while (i < text.length) {
|
|
206
|
+
if (text[i] === quote) {
|
|
207
|
+
if (text[i + 1] === quote) i += 2;
|
|
208
|
+
else return i + 1;
|
|
209
|
+
} else {
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return text.length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Normalization ----------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
function normalize(tokens) {
|
|
219
|
+
let out = '';
|
|
220
|
+
let space = false;
|
|
221
|
+
for (const token of tokens) {
|
|
222
|
+
if (token.kind === 'space' || token.kind === 'comment') {
|
|
223
|
+
space = out.length > 0;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (space) out += ' ';
|
|
227
|
+
space = false;
|
|
228
|
+
if (token.kind === 'string' || token.kind === 'number' || token.kind === 'placeholder') out += '?';
|
|
229
|
+
else if (token.kind === 'ident') out += token.text;
|
|
230
|
+
else out += token.text.toLowerCase();
|
|
231
|
+
}
|
|
232
|
+
return out
|
|
233
|
+
.replace(/\bin \(\s*\?(?:\s*,\s*\?)*\s*\)/g, 'in (?)')
|
|
234
|
+
.replace(/\bin\(\s*\?(?:\s*,\s*\?)*\s*\)/g, 'in(?)')
|
|
235
|
+
.replace(/\bvalues ?\(\s*\?(?:\s*,\s*\?)*\s*\)(?:\s*,\s*\(\s*\?(?:\s*,\s*\?)*\s*\))*/g, 'values (?)');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Columns ----------------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
// The column each placeholder (by index) and each string literal (by token) is bound to. A
|
|
241
|
+
// placeholder used twice ($1 in two places) is sensitive when either place is.
|
|
242
|
+
function bindColumns(tokens) {
|
|
243
|
+
const significant = tokens.filter((token) => token.kind !== 'space' && token.kind !== 'comment');
|
|
244
|
+
const columns = new Map();
|
|
245
|
+
const sensitive = new Set();
|
|
246
|
+
const literalColumns = new Map();
|
|
247
|
+
const bind = (token, column) => {
|
|
248
|
+
if (!column) return;
|
|
249
|
+
if (token.kind === 'placeholder') {
|
|
250
|
+
if (!columns.has(token.index)) columns.set(token.index, column);
|
|
251
|
+
if (Masker.isSensitiveName(column)) sensitive.add(token.index);
|
|
252
|
+
} else if (token.kind === 'string') {
|
|
253
|
+
if (!literalColumns.has(token) || Masker.isSensitiveName(column)) literalColumns.set(token, column);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < significant.length; i++) {
|
|
258
|
+
const token = significant[i];
|
|
259
|
+
if (token.kind !== 'placeholder' && token.kind !== 'string') continue;
|
|
260
|
+
bind(token, comparedColumn(significant, i) || reversedColumn(significant, i));
|
|
261
|
+
}
|
|
262
|
+
for (const { token, column } of insertedColumns(significant)) bind(token, column);
|
|
263
|
+
return { columns, sensitive, literalColumns };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// `X <op> <column>`: the value first, as in `where $1 = email`.
|
|
267
|
+
function reversedColumn(tokens, at) {
|
|
268
|
+
const op = tokens[at + 1];
|
|
269
|
+
if (!op || !COMPARISON.has(op.text)) return null;
|
|
270
|
+
const token = tokens[at + 2];
|
|
271
|
+
if (!token) return null;
|
|
272
|
+
// t.col: the last part.
|
|
273
|
+
let i = at + 2;
|
|
274
|
+
while (tokens[i + 1] && tokens[i + 1].text === '.' && tokens[i + 2] && (tokens[i + 2].kind === 'word' || tokens[i + 2].kind === 'ident')) i += 2;
|
|
275
|
+
const last = tokens[i];
|
|
276
|
+
if (last.kind === 'ident') return last.name;
|
|
277
|
+
if (last.kind === 'word') return last.text;
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// `<column> <op> X`, `<column> [not] like X`, `<column> [not] in (X, ...)`, `<column> = any(X)`.
|
|
282
|
+
function comparedColumn(tokens, at) {
|
|
283
|
+
let i = at - 1;
|
|
284
|
+
// Inside parentheses: in (...), any(...).
|
|
285
|
+
if (tokens[i] && (tokens[i].text === '(' || tokens[i].text === ',')) {
|
|
286
|
+
let depth = 0;
|
|
287
|
+
for (; i >= 0; i--) {
|
|
288
|
+
const text = tokens[i].text;
|
|
289
|
+
if (text === ')') depth++;
|
|
290
|
+
else if (text === '(') {
|
|
291
|
+
if (depth === 0) break;
|
|
292
|
+
depth--;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (i < 0) return null;
|
|
296
|
+
i--; // before "("
|
|
297
|
+
const word = tokens[i] && tokens[i].kind === 'word' ? tokens[i].text.toLowerCase() : '';
|
|
298
|
+
if (word === 'in') {
|
|
299
|
+
i--;
|
|
300
|
+
if (tokens[i] && tokens[i].kind === 'word' && tokens[i].text.toLowerCase() === 'not') i--;
|
|
301
|
+
return columnBefore(tokens, i);
|
|
302
|
+
}
|
|
303
|
+
if (QUANTIFIERS.has(word)) {
|
|
304
|
+
i--;
|
|
305
|
+
if (!tokens[i] || !COMPARISON.has(tokens[i].text)) return null;
|
|
306
|
+
return columnBefore(tokens, i - 1);
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const op = tokens[i];
|
|
312
|
+
if (!op) return null;
|
|
313
|
+
if (COMPARISON.has(op.text)) return columnBefore(tokens, i - 1);
|
|
314
|
+
if (op.kind === 'word' && COMPARISON_WORDS.has(op.text.toLowerCase())) {
|
|
315
|
+
i--;
|
|
316
|
+
if (tokens[i] && tokens[i].kind === 'word' && tokens[i].text.toLowerCase() === 'not') i--;
|
|
317
|
+
return columnBefore(tokens, i);
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// The column expression ending at `i`: `col`, `"col"`, `t.col`, `` `t`.`col` ``, `col::type`.
|
|
323
|
+
function columnBefore(tokens, i) {
|
|
324
|
+
// Skip a cast: col::text
|
|
325
|
+
if (tokens[i] && tokens[i].kind === 'word' && tokens[i - 1] && tokens[i - 1].text === '::') i -= 2;
|
|
326
|
+
const token = tokens[i];
|
|
327
|
+
if (!token) return null;
|
|
328
|
+
if (token.kind === 'ident') return token.name;
|
|
329
|
+
if (token.kind === 'word') return token.text;
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// insert into t (c1, c2) values (X, Y), (X, Y): each value is bound to the column at its position.
|
|
334
|
+
function insertedColumns(tokens) {
|
|
335
|
+
const bound = [];
|
|
336
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
337
|
+
if (tokens[i].kind !== 'word' || tokens[i].text.toLowerCase() !== 'insert') continue;
|
|
338
|
+
// The column list: the first "(" after the table name, before "values".
|
|
339
|
+
let j = i + 1;
|
|
340
|
+
while (j < tokens.length && tokens[j].text !== '(' && !(tokens[j].kind === 'word' && /^(values|select|set|default)$/i.test(tokens[j].text))) j++;
|
|
341
|
+
if (!tokens[j] || tokens[j].text !== '(') continue;
|
|
342
|
+
const names = [];
|
|
343
|
+
j++;
|
|
344
|
+
for (; j < tokens.length && tokens[j].text !== ')'; j++) {
|
|
345
|
+
if (tokens[j].kind === 'ident') names.push(tokens[j].name);
|
|
346
|
+
else if (tokens[j].kind === 'word') {
|
|
347
|
+
// t.col: the last part wins.
|
|
348
|
+
if (tokens[j - 1] && tokens[j - 1].text === '.') names[names.length - 1] = tokens[j].text;
|
|
349
|
+
else names.push(tokens[j].text);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
j++;
|
|
353
|
+
if (!tokens[j] || tokens[j].kind !== 'word' || !/^values?$/i.test(tokens[j].text)) continue;
|
|
354
|
+
j++;
|
|
355
|
+
// Tuples: ( a, b ), ( c, d )
|
|
356
|
+
while (tokens[j] && tokens[j].text === '(') {
|
|
357
|
+
let depth = 0;
|
|
358
|
+
let position = 0;
|
|
359
|
+
for (j++; j < tokens.length; j++) {
|
|
360
|
+
const text = tokens[j].text;
|
|
361
|
+
if (text === '(') depth++;
|
|
362
|
+
else if (text === ')') {
|
|
363
|
+
if (depth === 0) break;
|
|
364
|
+
depth--;
|
|
365
|
+
} else if (text === ',' && depth === 0) {
|
|
366
|
+
position++;
|
|
367
|
+
} else if (tokens[j].kind === 'placeholder' || tokens[j].kind === 'string') {
|
|
368
|
+
if (names[position]) bound.push({ token: tokens[j], column: names[position] });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
j++;
|
|
372
|
+
if (tokens[j] && tokens[j].text === ',') j++;
|
|
373
|
+
else break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return bound;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// --- Masked text ------------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
// Replaces literals bound to sensitive columns, and literals that look like secrets, with
|
|
382
|
+
// '[masked]'. Returns the source itself when nothing needed masking.
|
|
383
|
+
function maskLiterals(source, tokens, literalColumns) {
|
|
384
|
+
let out = '';
|
|
385
|
+
let last = 0;
|
|
386
|
+
for (const token of tokens) {
|
|
387
|
+
if (token.kind !== 'string') continue;
|
|
388
|
+
const column = literalColumns.get(token);
|
|
389
|
+
const value = token.text.replace(/^[Ee]?['"]|['"]$/g, '');
|
|
390
|
+
if ((column && Masker.isSensitiveName(column)) || Masker.isSecretString(value)) {
|
|
391
|
+
out += `${source.slice(last, token.start)}'[masked]'`;
|
|
392
|
+
last = token.end;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return last === 0 ? source : out + source.slice(last);
|
|
396
|
+
}
|