@mnemonik/shared 7.144.2 → 7.145.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast/grammars.d.ts +6 -6
- package/dist/ast/grammars.d.ts.map +1 -1
- package/dist/ast/grammars.js +51 -10
- package/dist/ast/grammars.js.map +1 -1
- package/dist/sourceRedaction.d.ts.map +1 -1
- package/dist/sourceRedaction.js +743 -64
- package/dist/sourceRedaction.js.map +1 -1
- package/package.json +2 -1
- package/queries/clojure.tags.scm +1 -0
- package/queries/dart.tags.scm +3 -0
- package/queries/erlang.tags.scm +2 -0
- package/queries/graphql.tags.scm +3 -0
- package/queries/haskell.tags.scm +3 -0
- package/queries/julia.tags.scm +1 -0
- package/queries/kotlin.tags.scm +3 -0
- package/queries/ocaml.tags.scm +2 -0
- package/queries/ocaml_interface.tags.scm +1 -0
- package/queries/perl.tags.scm +2 -0
- package/queries/protobuf.tags.scm +5 -0
- package/queries/r.tags.scm +2 -0
- package/queries/zig.tags.scm +2 -0
- package/scripts/vendor-grammars.ts +586 -35
- package/src/ast/grammars.ts +51 -10
- package/src/sourceRedaction.ts +765 -12
- package/wasm/bash.wasm.gz +0 -0
- package/wasm/c-sharp.wasm.gz +0 -0
- package/wasm/c.wasm.gz +0 -0
- package/wasm/clojure.LICENSE +21 -0
- package/wasm/clojure.tags.scm +1 -0
- package/wasm/clojure.wasm.gz +0 -0
- package/wasm/cmake.wasm.gz +0 -0
- package/wasm/cpp.wasm.gz +0 -0
- package/wasm/dart.LICENSE +20 -0
- package/wasm/dart.tags.scm +3 -0
- package/wasm/dart.wasm.gz +0 -0
- package/wasm/elixir.wasm.gz +0 -0
- package/wasm/erlang.LICENSE +201 -0
- package/wasm/erlang.tags.scm +2 -0
- package/wasm/erlang.wasm.gz +0 -0
- package/wasm/go.wasm.gz +0 -0
- package/wasm/graphql.LICENSE +21 -0
- package/wasm/graphql.tags.scm +3 -0
- package/wasm/graphql.wasm.gz +0 -0
- package/wasm/groovy.wasm.gz +0 -0
- package/wasm/haskell.LICENSE +21 -0
- package/wasm/haskell.tags.scm +3 -0
- package/wasm/haskell.wasm.gz +0 -0
- package/wasm/java.wasm.gz +0 -0
- package/wasm/javascript.wasm.gz +0 -0
- package/wasm/julia.LICENSE +21 -0
- package/wasm/julia.tags.scm +1 -0
- package/wasm/julia.wasm.gz +0 -0
- package/wasm/kotlin.LICENSE +9 -0
- package/wasm/kotlin.tags.scm +3 -0
- package/wasm/kotlin.wasm.gz +0 -0
- package/wasm/lua.wasm.gz +0 -0
- package/wasm/nix.wasm.gz +0 -0
- package/wasm/objcpp.wasm.gz +0 -0
- package/wasm/ocaml.LICENSE +21 -0
- package/wasm/ocaml.tags.scm +2 -0
- package/wasm/ocaml.wasm.gz +0 -0
- package/wasm/ocaml_interface.LICENSE +21 -0
- package/wasm/ocaml_interface.tags.scm +1 -0
- package/wasm/ocaml_interface.wasm.gz +0 -0
- package/wasm/perl.LICENSE +7 -0
- package/wasm/perl.tags.scm +2 -0
- package/wasm/perl.wasm.gz +0 -0
- package/wasm/php.wasm.gz +0 -0
- package/wasm/powershell.wasm.gz +0 -0
- package/wasm/protobuf.LICENSE +21 -0
- package/wasm/protobuf.tags.scm +5 -0
- package/wasm/protobuf.wasm.gz +0 -0
- package/wasm/python.wasm.gz +0 -0
- package/wasm/r.LICENSE +9 -0
- package/wasm/r.tags.scm +2 -0
- package/wasm/r.wasm.gz +0 -0
- package/wasm/ruby.wasm.gz +0 -0
- package/wasm/rust.wasm.gz +0 -0
- package/wasm/scala.wasm.gz +0 -0
- package/wasm/solidity.wasm.gz +0 -0
- package/wasm/sql.wasm.gz +0 -0
- package/wasm/swift.wasm.gz +0 -0
- package/wasm/terraform.wasm.gz +0 -0
- package/wasm/tsx.wasm.gz +0 -0
- package/wasm/typescript.wasm.gz +0 -0
- package/wasm/zig.LICENSE +29 -0
- package/wasm/zig.tags.scm +2 -0
- package/wasm/zig.wasm.gz +0 -0
- package/wasm/bash.wasm +0 -0
- package/wasm/c-sharp.wasm +0 -0
- package/wasm/c.wasm +0 -0
- package/wasm/cmake.wasm +0 -0
- package/wasm/cpp.wasm +0 -0
- package/wasm/elixir.wasm +0 -0
- package/wasm/go.wasm +0 -0
- package/wasm/groovy.wasm +0 -0
- package/wasm/java.wasm +0 -0
- package/wasm/javascript.wasm +0 -0
- package/wasm/lua.wasm +0 -0
- package/wasm/nix.wasm +0 -0
- package/wasm/objcpp.wasm +0 -0
- package/wasm/php.wasm +0 -0
- package/wasm/powershell.wasm +0 -0
- package/wasm/python.wasm +0 -0
- package/wasm/ruby.wasm +0 -0
- package/wasm/rust.wasm +0 -0
- package/wasm/scala.wasm +0 -0
- package/wasm/solidity.wasm +0 -0
- package/wasm/sql.wasm +0 -0
- package/wasm/swift.wasm +0 -0
- package/wasm/terraform.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
package/src/sourceRedaction.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseVueSource, walkVueTemplate, VUE_FIDELITY_MARKER } from './vueSource.js';
|
|
2
2
|
import { basename, extname } from 'node:path';
|
|
3
3
|
import { Parser, type Node, type Tree } from 'web-tree-sitter';
|
|
4
|
-
import { loadGrammar, objcppSyntaxIssue } from './ast/grammars.js';
|
|
4
|
+
import { loadGrammar, objcppSyntaxIssue, type AstLanguageId } from './ast/grammars.js';
|
|
5
5
|
import { MAX_AST_PARSE_BYTES } from './ast/astChunker.js';
|
|
6
6
|
import { commandLineCredentialRanges, redactCredentials } from './redactCredentials.js';
|
|
7
7
|
import {
|
|
@@ -19,8 +19,42 @@ function scrubPayload(text: string): string {
|
|
|
19
19
|
export async function scrubSourceSecrets(text: string, filePath: string): Promise<string> {
|
|
20
20
|
const extension = extname(filePath).toLowerCase();
|
|
21
21
|
if (extension === '.vue') return scrubVueSource(text);
|
|
22
|
+
if (extension === '.svelte') return scrubSvelteSource(text);
|
|
23
|
+
const bundledLanguages: Partial<Record<string, AstLanguageId>> = {
|
|
24
|
+
'.go': 'go',
|
|
25
|
+
'.java': 'java',
|
|
26
|
+
'.cs': 'csharp',
|
|
27
|
+
'.scala': 'scala',
|
|
28
|
+
'.sc': 'scala',
|
|
29
|
+
'.rb': 'ruby',
|
|
30
|
+
'.php': 'php',
|
|
31
|
+
'.ex': 'elixir',
|
|
32
|
+
'.exs': 'elixir',
|
|
33
|
+
'.sol': 'solidity',
|
|
34
|
+
'.kt': 'kotlin',
|
|
35
|
+
'.kts': 'kotlin',
|
|
36
|
+
'.dart': 'dart',
|
|
37
|
+
'.erl': 'erlang',
|
|
38
|
+
'.hrl': 'erlang',
|
|
39
|
+
'.hs': 'haskell',
|
|
40
|
+
'.ml': 'ocaml',
|
|
41
|
+
'.mli': 'ocaml_interface',
|
|
42
|
+
'.clj': 'clojure',
|
|
43
|
+
'.cljs': 'clojure',
|
|
44
|
+
'.cljc': 'clojure',
|
|
45
|
+
'.jl': 'julia',
|
|
46
|
+
'.pl': 'perl',
|
|
47
|
+
'.pm': 'perl',
|
|
48
|
+
'.r': 'r',
|
|
49
|
+
'.zig': 'zig',
|
|
50
|
+
'.proto': 'protobuf',
|
|
51
|
+
'.graphql': 'graphql',
|
|
52
|
+
'.gql': 'graphql',
|
|
53
|
+
};
|
|
54
|
+
const bundledLanguage = bundledLanguages[extension];
|
|
22
55
|
const language =
|
|
23
|
-
|
|
56
|
+
bundledLanguage ??
|
|
57
|
+
(extension === '.cmake' || basename(filePath) === 'CMakeLists.txt'
|
|
24
58
|
? 'cmake'
|
|
25
59
|
: ['.ts', '.mts', '.cts'].includes(extension)
|
|
26
60
|
? 'typescript'
|
|
@@ -28,7 +62,7 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
|
|
|
28
62
|
? 'tsx'
|
|
29
63
|
: ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
|
|
30
64
|
? 'javascript'
|
|
31
|
-
: ['.py', '.pyi'].includes(extension)
|
|
65
|
+
: ['.py', '.pyi', '.bzl'].includes(extension)
|
|
32
66
|
? 'python'
|
|
33
67
|
: ['.sh', '.bash', '.zsh'].includes(extension)
|
|
34
68
|
? 'bash'
|
|
@@ -56,10 +90,34 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
|
|
|
56
90
|
? 'groovy'
|
|
57
91
|
: ['.ps1', '.psm1'].includes(extension)
|
|
58
92
|
? 'powershell'
|
|
59
|
-
: null;
|
|
60
|
-
const fallback = (): string => {
|
|
61
|
-
|
|
93
|
+
: null);
|
|
94
|
+
const fallback = (source = text, payloadsScrubbed = false): string => {
|
|
95
|
+
// A completed AST payload pass must not be reinterpreted as prose (e.g. proto field numbers).
|
|
96
|
+
const clean = payloadsScrubbed ? source : scrubPayload(source);
|
|
62
97
|
if (!text) return clean;
|
|
98
|
+
if (bundledLanguage) {
|
|
99
|
+
const marker = `/* mnemonik:${bundledLanguage}-source-fidelity-unavailable */`;
|
|
100
|
+
const nativeMarker: Partial<Record<AstLanguageId, string>> = {
|
|
101
|
+
erlang: `% mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
|
|
102
|
+
haskell: `-- mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
|
|
103
|
+
ocaml: `(* mnemonik:${bundledLanguage}-source-fidelity-unavailable *)`,
|
|
104
|
+
ocaml_interface: `(* mnemonik:${bundledLanguage}-source-fidelity-unavailable *)`,
|
|
105
|
+
clojure: `; mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
|
|
106
|
+
zig: `// mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
|
|
107
|
+
};
|
|
108
|
+
const comment =
|
|
109
|
+
nativeMarker[bundledLanguage] ??
|
|
110
|
+
(['ruby', 'elixir', 'julia', 'perl', 'r', 'graphql'].includes(bundledLanguage)
|
|
111
|
+
? marker.replace('/* ', '# ').replace(' */', '')
|
|
112
|
+
: bundledLanguage === 'php'
|
|
113
|
+
? `<?php ${marker} ?>`
|
|
114
|
+
: marker);
|
|
115
|
+
return clean.startsWith(comment) ? clean : comment + '\n' + clean;
|
|
116
|
+
}
|
|
117
|
+
if (extension === '.bzl') {
|
|
118
|
+
const marker = '# mnemonik:starlark-source-fidelity-unavailable\n';
|
|
119
|
+
return clean.startsWith(marker) ? clean : marker + clean;
|
|
120
|
+
}
|
|
63
121
|
if (language === 'powershell') {
|
|
64
122
|
const marker = '# mnemonik:powershell-source-fidelity-unavailable\n';
|
|
65
123
|
return clean.startsWith(marker) ? clean : marker + clean;
|
|
@@ -89,12 +147,20 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
|
|
|
89
147
|
parser = new Parser();
|
|
90
148
|
parser.setLanguage(grammar.ok.language);
|
|
91
149
|
tree = parser.parse(text);
|
|
92
|
-
if (!tree
|
|
150
|
+
if (!tree) return fallback();
|
|
151
|
+
if (tree.rootNode.hasError) {
|
|
152
|
+
// These bindings have no prose-style credential '=' label. Recover AST-proven payloads;
|
|
153
|
+
// opaque ERROR spans are scrubbed conservatively and always remain unavailable.
|
|
154
|
+
return bundledLanguage && ['clojure', 'r', 'protobuf', 'graphql'].includes(bundledLanguage)
|
|
155
|
+
? fallback(redactBundledNode(tree.rootNode, bundledLanguage, true), true)
|
|
156
|
+
: fallback();
|
|
157
|
+
}
|
|
93
158
|
const root = tree.rootNode;
|
|
94
159
|
if (language === 'objcpp' && objcppSyntaxIssue(root)) return fallback();
|
|
95
160
|
|
|
96
|
-
const redacted =
|
|
97
|
-
|
|
161
|
+
const redacted = bundledLanguage
|
|
162
|
+
? redactBundledNode(root, bundledLanguage)
|
|
163
|
+
: language === 'cmake'
|
|
98
164
|
? redactCmakeNode(root)
|
|
99
165
|
: language === 'bash'
|
|
100
166
|
? redactShellNode(root, commandLineCredentialRanges(text))
|
|
@@ -117,7 +183,16 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
|
|
|
117
183
|
: language === 'powershell'
|
|
118
184
|
? redactPowershellNode(root)
|
|
119
185
|
: redactNode(root);
|
|
120
|
-
|
|
186
|
+
const clean = text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
|
|
187
|
+
if (bundledLanguage && clean !== text) {
|
|
188
|
+
const proof = parser.parse(clean);
|
|
189
|
+
try {
|
|
190
|
+
if (!proof || proof.rootNode.hasError) return fallback(clean);
|
|
191
|
+
} finally {
|
|
192
|
+
proof?.delete();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return clean;
|
|
121
196
|
} catch {
|
|
122
197
|
return fallback();
|
|
123
198
|
} finally {
|
|
@@ -126,6 +201,531 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
|
|
|
126
201
|
}
|
|
127
202
|
}
|
|
128
203
|
|
|
204
|
+
/** Bundled L1 grammars expose literal payloads separately from executable expressions. */
|
|
205
|
+
function redactBundledNode(root: Node, language: AstLanguageId, incomplete = false): string {
|
|
206
|
+
const labeled = new Set<number>();
|
|
207
|
+
const heredocs: boolean[] = [];
|
|
208
|
+
const label = (node: Node | null): boolean => {
|
|
209
|
+
if (!node) return false;
|
|
210
|
+
if (language === 'julia' && node.type === 'typed_expression')
|
|
211
|
+
return label(node.firstNamedChild);
|
|
212
|
+
if (language === 'perl' && node.type === 'variable_declaration')
|
|
213
|
+
return label(node.childForFieldName('variable'));
|
|
214
|
+
if (language === 'perl' && node.type === 'scalar') return label(node.firstNamedChild);
|
|
215
|
+
if (
|
|
216
|
+
language === 'kotlin' &&
|
|
217
|
+
['navigation_expression', 'directly_assignable_expression'].includes(node.type)
|
|
218
|
+
) {
|
|
219
|
+
const last = node.lastNamedChild;
|
|
220
|
+
return label(
|
|
221
|
+
last?.type === 'navigation_suffix' ? last.firstNamedChild : node.firstNamedChild
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (language === 'dart' && node.type === 'assignable_expression') {
|
|
225
|
+
const last = node.lastNamedChild;
|
|
226
|
+
return label(
|
|
227
|
+
last?.type === 'unconditional_assignable_selector'
|
|
228
|
+
? last.firstNamedChild
|
|
229
|
+
: node.firstNamedChild
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
[
|
|
234
|
+
'expression',
|
|
235
|
+
'literal_element',
|
|
236
|
+
'variable_name',
|
|
237
|
+
'assignable_expression',
|
|
238
|
+
'directly_assignable_expression',
|
|
239
|
+
].includes(node.type)
|
|
240
|
+
)
|
|
241
|
+
return label(node.firstNamedChild);
|
|
242
|
+
const field =
|
|
243
|
+
node.childForFieldName('field') ??
|
|
244
|
+
node.childForFieldName('member') ??
|
|
245
|
+
node.childForFieldName('property') ??
|
|
246
|
+
node.childForFieldName('name') ??
|
|
247
|
+
node;
|
|
248
|
+
return SECRET_LABEL_RE.test(
|
|
249
|
+
field.text
|
|
250
|
+
.trim()
|
|
251
|
+
.replace(/^[$@]+|["':]+$/g, '')
|
|
252
|
+
.replace(/^["']/, '')
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
const mark = (key: Node | null, value: Node | null): void => {
|
|
256
|
+
if (!key || !value) return;
|
|
257
|
+
if (label(key)) labeled.add(value.id);
|
|
258
|
+
else if (key.type === 'tuple_pattern' && value.type === 'tuple_expression')
|
|
259
|
+
key.namedChildren.forEach((part, i) => mark(part, value.namedChildren[i] ?? null));
|
|
260
|
+
};
|
|
261
|
+
const payload = (text: string, sensitive: boolean): string =>
|
|
262
|
+
scrubPayload(text)
|
|
263
|
+
.split(/(\r\n|[\r\n])/)
|
|
264
|
+
.map((part, i) =>
|
|
265
|
+
i % 2
|
|
266
|
+
? part
|
|
267
|
+
: part.replace(
|
|
268
|
+
/^(\s*)([\s\S]*?)(\s*)$/,
|
|
269
|
+
(_match, lead: string, body: string, tail: string) =>
|
|
270
|
+
lead + literalPayload(body, sensitive) + tail
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
.join('');
|
|
274
|
+
const visit = (node: Node, sensitive = false): string => {
|
|
275
|
+
sensitive ||= labeled.has(node.id);
|
|
276
|
+
const original = node.text;
|
|
277
|
+
const type = node.type;
|
|
278
|
+
if (incomplete && type === 'ERROR') return redactSecretValue(original);
|
|
279
|
+
if (
|
|
280
|
+
[
|
|
281
|
+
'comment',
|
|
282
|
+
'line_comment',
|
|
283
|
+
'block_comment',
|
|
284
|
+
'documentation_comment',
|
|
285
|
+
'multiline_comment',
|
|
286
|
+
].includes(type)
|
|
287
|
+
) {
|
|
288
|
+
if (language === 'clojure') {
|
|
289
|
+
if (original.startsWith(';')) return ';' + scrubPayload(original.slice(1));
|
|
290
|
+
if (original.startsWith('#!')) return '#!' + scrubPayload(original.slice(2));
|
|
291
|
+
// Reader-discard and (comment ...) contain forms, not line-comment payloads.
|
|
292
|
+
let clean = '';
|
|
293
|
+
let offset = node.startIndex;
|
|
294
|
+
for (const child of node.namedChildren) {
|
|
295
|
+
clean +=
|
|
296
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex) +
|
|
297
|
+
visit(child);
|
|
298
|
+
offset = child.endIndex;
|
|
299
|
+
}
|
|
300
|
+
return clean + original.slice(offset - node.startIndex);
|
|
301
|
+
}
|
|
302
|
+
if (language === 'erlang') return '%' + scrubPayload(original.slice(1));
|
|
303
|
+
if (language === 'haskell')
|
|
304
|
+
return original.startsWith('{-')
|
|
305
|
+
? '{-' + scrubPayload(original.slice(2, -2)) + '-}'
|
|
306
|
+
: '--' + scrubPayload(original.slice(2));
|
|
307
|
+
if (language === 'ocaml' || language === 'ocaml_interface')
|
|
308
|
+
return '(*' + scrubPayload(original.slice(2, -2)) + '*)';
|
|
309
|
+
if (original.startsWith('#')) return '#' + scrubPayload(original.slice(1));
|
|
310
|
+
return original.startsWith('/*')
|
|
311
|
+
? '/*' + scrubPayload(original.slice(2, -2)) + '*/'
|
|
312
|
+
: '//' + scrubPayload(original.slice(2));
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
['erlang', 'haskell', 'ocaml', 'ocaml_interface', 'clojure'].includes(language) &&
|
|
316
|
+
['char', 'character'].includes(type)
|
|
317
|
+
) {
|
|
318
|
+
if (sensitive || scrubPayload(original) !== original)
|
|
319
|
+
throw new Error('Restricted literal payload');
|
|
320
|
+
return original;
|
|
321
|
+
}
|
|
322
|
+
if ((language === 'ocaml' || language === 'ocaml_interface') && type === 'quoted_string') {
|
|
323
|
+
const opening = /^\{([a-z_]*)\|/.exec(original);
|
|
324
|
+
const closing = opening ? '|' + opening[1] + '}' : '';
|
|
325
|
+
if (!opening || !original.endsWith(closing))
|
|
326
|
+
throw new Error('Opaque quoted literal boundary');
|
|
327
|
+
return (
|
|
328
|
+
opening[0] +
|
|
329
|
+
payload(original.slice(opening[0].length, -closing.length), sensitive) +
|
|
330
|
+
closing
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (language === 'perl' && type === 'interpolated_string_literal') {
|
|
334
|
+
const content = node.childForFieldName('content');
|
|
335
|
+
return content
|
|
336
|
+
? original.slice(0, content.startIndex - node.startIndex) +
|
|
337
|
+
visit(content, sensitive) +
|
|
338
|
+
original.slice(content.endIndex - node.startIndex)
|
|
339
|
+
: original;
|
|
340
|
+
}
|
|
341
|
+
if (language === 'perl' && type === 'string_content') {
|
|
342
|
+
let clean = '';
|
|
343
|
+
let offset = node.startIndex;
|
|
344
|
+
for (const child of node.namedChildren.filter(
|
|
345
|
+
(c) => !['escape_sequence', 'escaped_delimiter'].includes(c.type)
|
|
346
|
+
)) {
|
|
347
|
+
clean +=
|
|
348
|
+
payload(
|
|
349
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex),
|
|
350
|
+
sensitive
|
|
351
|
+
) + visit(child);
|
|
352
|
+
offset = child.endIndex;
|
|
353
|
+
}
|
|
354
|
+
return clean + payload(original.slice(offset - node.startIndex), sensitive);
|
|
355
|
+
}
|
|
356
|
+
if (type === 'heredoc_beginning') {
|
|
357
|
+
heredocs.push(sensitive);
|
|
358
|
+
return original;
|
|
359
|
+
}
|
|
360
|
+
if (language === 'ruby' && type === 'heredoc_body') sensitive = heredocs.shift() ?? false;
|
|
361
|
+
if (type === 'hex_string_literal')
|
|
362
|
+
return sensitive
|
|
363
|
+
? original.replace(
|
|
364
|
+
/(["'])([0-9a-fA-F_\s]*)\1/g,
|
|
365
|
+
(_match, quote: string, body: string) =>
|
|
366
|
+
quote + body.replace(/[0-9a-fA-F_]/g, '') + quote
|
|
367
|
+
)
|
|
368
|
+
: original;
|
|
369
|
+
// shortcut: restricted character/symbol literals use fallback; add native placeholders if needed.
|
|
370
|
+
if (['character_literal', 'rune_literal', 'simple_symbol', 'atom'].includes(type)) {
|
|
371
|
+
if (sensitive || scrubPayload(original) !== original)
|
|
372
|
+
throw new Error('Restricted literal payload');
|
|
373
|
+
return original;
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
sensitive &&
|
|
377
|
+
/^(?:int|float|imaginary|integer|real|decimal_integer|hex_integer|octal_integer|binary_integer|decimal_floating_point|hex_floating_point|number)(?:_literal)?$/.test(
|
|
378
|
+
type
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
return '"' + redactSecretValue(original) + '"';
|
|
382
|
+
const string =
|
|
383
|
+
[
|
|
384
|
+
'string',
|
|
385
|
+
'interpreted_string_literal',
|
|
386
|
+
'raw_string_literal',
|
|
387
|
+
'verbatim_string_literal',
|
|
388
|
+
'encapsed_string',
|
|
389
|
+
'unicode_string_literal',
|
|
390
|
+
'string_value',
|
|
391
|
+
'interpolated_string',
|
|
392
|
+
'sigil',
|
|
393
|
+
].includes(type) ||
|
|
394
|
+
(type === 'string_literal' && language !== 'solidity');
|
|
395
|
+
if (string) {
|
|
396
|
+
const first = node.firstChild;
|
|
397
|
+
const last = node.lastChild;
|
|
398
|
+
const quotedStart =
|
|
399
|
+
node.childForFieldName('quoted_start') ??
|
|
400
|
+
(language === 'r' ? node.childForFieldName('open') : null);
|
|
401
|
+
const quotedEnd =
|
|
402
|
+
node.childForFieldName('quoted_end') ??
|
|
403
|
+
(language === 'r' ? node.childForFieldName('close') : null);
|
|
404
|
+
const opening = quotedStart ?? first;
|
|
405
|
+
const closing = quotedEnd ?? last;
|
|
406
|
+
const match = /^(?:r|@|unicode)?("{3,}|'{3,}|"|'|`)/.exec(original);
|
|
407
|
+
const start = quotedStart
|
|
408
|
+
? quotedStart.endIndex
|
|
409
|
+
: opening && /^(?:%[qQwWiIxrs]?[^\w\s]|["'`]+)$/.test(opening.text)
|
|
410
|
+
? opening.endIndex
|
|
411
|
+
: match
|
|
412
|
+
? node.startIndex + match[0].length
|
|
413
|
+
: null;
|
|
414
|
+
const end = quotedEnd
|
|
415
|
+
? quotedEnd.startIndex
|
|
416
|
+
: match?.[1]
|
|
417
|
+
? node.endIndex - match[1].length
|
|
418
|
+
: closing && closing.id !== opening?.id
|
|
419
|
+
? closing.startIndex
|
|
420
|
+
: null;
|
|
421
|
+
if (start === null || end === null || end < start) throw new Error('Opaque literal boundary');
|
|
422
|
+
const substitutions = node.children.filter(
|
|
423
|
+
(child) =>
|
|
424
|
+
child.startIndex >= start &&
|
|
425
|
+
child.endIndex <= end &&
|
|
426
|
+
![
|
|
427
|
+
'string_content',
|
|
428
|
+
'content',
|
|
429
|
+
'string_fragment',
|
|
430
|
+
'multiline_string_fragment',
|
|
431
|
+
'raw_string_content',
|
|
432
|
+
'raw_string_literal_content',
|
|
433
|
+
'interpreted_string_literal_content',
|
|
434
|
+
'string_literal_content',
|
|
435
|
+
'quoted_content',
|
|
436
|
+
'escape_sequence',
|
|
437
|
+
].includes(child.type)
|
|
438
|
+
);
|
|
439
|
+
let result = original.slice(0, start - node.startIndex);
|
|
440
|
+
let offset = start;
|
|
441
|
+
for (const child of substitutions) {
|
|
442
|
+
result += payload(
|
|
443
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex),
|
|
444
|
+
sensitive
|
|
445
|
+
);
|
|
446
|
+
result += visit(child); // Runtime interpolation never inherits credential sensitivity.
|
|
447
|
+
offset = child.endIndex;
|
|
448
|
+
}
|
|
449
|
+
return (
|
|
450
|
+
result +
|
|
451
|
+
payload(original.slice(offset - node.startIndex, end - node.startIndex), sensitive) +
|
|
452
|
+
original.slice(end - node.startIndex)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (['string_content', 'heredoc_content', 'quoted_content', 'nowdoc_body'].includes(type))
|
|
456
|
+
return payload(original, sensitive);
|
|
457
|
+
if (language === 'julia' && type === 'assignment')
|
|
458
|
+
mark(node.firstNamedChild, node.lastNamedChild);
|
|
459
|
+
if (language === 'r' && type === 'binary_operator') {
|
|
460
|
+
const op = node.childForFieldName('operator')?.text;
|
|
461
|
+
if (['<-', '<<-', '=', ':='].includes(op ?? ''))
|
|
462
|
+
mark(node.childForFieldName('lhs'), node.childForFieldName('rhs'));
|
|
463
|
+
if (['->', '->>'].includes(op ?? ''))
|
|
464
|
+
mark(node.childForFieldName('rhs'), node.childForFieldName('lhs'));
|
|
465
|
+
}
|
|
466
|
+
if (language === 'r' && ['parameter', 'argument'].includes(type))
|
|
467
|
+
mark(
|
|
468
|
+
node.childForFieldName('name'),
|
|
469
|
+
node.childForFieldName('default') ?? node.childForFieldName('value')
|
|
470
|
+
);
|
|
471
|
+
if (language === 'zig' && ['assignment_statement', 'field_declaration'].includes(type))
|
|
472
|
+
mark(
|
|
473
|
+
node.childForFieldName('name'),
|
|
474
|
+
node.childForFieldName('expression') ?? node.childForFieldName('default')
|
|
475
|
+
);
|
|
476
|
+
if (language === 'protobuf' && type === 'field') {
|
|
477
|
+
const name = node.namedChildren.find((c) => c.type === 'identifier');
|
|
478
|
+
for (const option of node.namedChildren.find((c) => c.type === 'field_options')
|
|
479
|
+
?.namedChildren ?? []) {
|
|
480
|
+
if (
|
|
481
|
+
option.type !== 'field_option' ||
|
|
482
|
+
option.namedChildren.find((c) => c.type === 'identifier')?.text !== 'default'
|
|
483
|
+
)
|
|
484
|
+
continue;
|
|
485
|
+
const constant = option.namedChildren.find((c) => c.type === 'constant');
|
|
486
|
+
mark(name ?? null, constant?.namedChildren.find((c) => c.type === 'string') ?? null);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (language === 'graphql' && type === 'object_field')
|
|
490
|
+
mark(
|
|
491
|
+
node.namedChildren.find((c) => c.type === 'name') ?? null,
|
|
492
|
+
node.namedChildren.find((c) => c.type === 'value') ?? null
|
|
493
|
+
);
|
|
494
|
+
if (
|
|
495
|
+
language === 'graphql' &&
|
|
496
|
+
['input_value_definition', 'variable_definition'].includes(type)
|
|
497
|
+
) {
|
|
498
|
+
const name =
|
|
499
|
+
type === 'input_value_definition'
|
|
500
|
+
? node.namedChildren.find((c) => c.type === 'name')
|
|
501
|
+
: node.namedChildren.find((c) => c.type === 'variable')?.firstNamedChild;
|
|
502
|
+
const value = node.namedChildren.find((c) => c.type === 'default_value')?.firstNamedChild;
|
|
503
|
+
mark(name ?? null, value ?? null);
|
|
504
|
+
}
|
|
505
|
+
if (language === 'erlang' && type === 'match_expr')
|
|
506
|
+
mark(node.childForFieldName('lhs'), node.childForFieldName('rhs'));
|
|
507
|
+
if (language === 'haskell' && type === 'bind') {
|
|
508
|
+
const name = node.childForFieldName('name');
|
|
509
|
+
mark(name, node.childForFieldName('expression'));
|
|
510
|
+
for (const match of node.childrenForFieldName('match'))
|
|
511
|
+
mark(name, match.childForFieldName('expression'));
|
|
512
|
+
}
|
|
513
|
+
if ((language === 'ocaml' || language === 'ocaml_interface') && type === 'let_binding')
|
|
514
|
+
mark(node.childForFieldName('pattern'), node.childForFieldName('body'));
|
|
515
|
+
if (language === 'clojure' && type === 'list') {
|
|
516
|
+
const forms = node.namedChildren.filter((c) => c.type !== 'comment');
|
|
517
|
+
if (
|
|
518
|
+
forms[0]?.type === 'symbol' &&
|
|
519
|
+
forms[0].text === 'def' &&
|
|
520
|
+
forms[1]?.type === 'symbol' &&
|
|
521
|
+
forms.length >= 3
|
|
522
|
+
)
|
|
523
|
+
mark(forms[1], forms.at(-1) ?? null);
|
|
524
|
+
if (forms[0]?.type === 'symbol' && forms[0].text === 'let' && forms[1]?.type === 'vector') {
|
|
525
|
+
const bindings = forms[1].namedChildren.filter((c) => c.type !== 'comment');
|
|
526
|
+
for (let i = 0; i + 1 < bindings.length; i += 2)
|
|
527
|
+
mark(bindings[i] ?? null, bindings[i + 1] ?? null);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (language === 'kotlin' && type === 'assignment')
|
|
531
|
+
mark(node.firstNamedChild, node.lastNamedChild);
|
|
532
|
+
if (language === 'dart' && type === 'assignment_expression_without_cascade')
|
|
533
|
+
mark(node.childForFieldName('left'), node.childForFieldName('right'));
|
|
534
|
+
if (language === 'kotlin' && type === 'property_declaration') {
|
|
535
|
+
const key = node.namedChildren.find(
|
|
536
|
+
(c) => c.type === 'variable_declaration'
|
|
537
|
+
)?.firstNamedChild;
|
|
538
|
+
const equals = node.children.findIndex((c) => c.type === '=');
|
|
539
|
+
if (equals >= 0) mark(key ?? null, node.children[equals + 1] ?? null);
|
|
540
|
+
}
|
|
541
|
+
if (
|
|
542
|
+
language === 'dart' &&
|
|
543
|
+
[
|
|
544
|
+
'initialized_variable_definition',
|
|
545
|
+
'initialized_identifier',
|
|
546
|
+
'static_final_declaration',
|
|
547
|
+
].includes(type)
|
|
548
|
+
) {
|
|
549
|
+
const equals = node.children.findIndex((c) => c.type === '=');
|
|
550
|
+
if (equals >= 0)
|
|
551
|
+
mark(
|
|
552
|
+
node.childForFieldName('name') ?? node.firstNamedChild,
|
|
553
|
+
node.children[equals + 1] ?? null
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
// These grammars place defaults beside the parameter rather than inside it.
|
|
557
|
+
if (
|
|
558
|
+
(language === 'kotlin' && type === 'function_value_parameters') ||
|
|
559
|
+
(language === 'dart' && type === 'optional_formal_parameters')
|
|
560
|
+
) {
|
|
561
|
+
node.children.forEach((child, i, children) => {
|
|
562
|
+
if (!['=', ':'].includes(child.type)) return;
|
|
563
|
+
const parameter = children[i - 1];
|
|
564
|
+
mark(
|
|
565
|
+
parameter?.childForFieldName('name') ?? parameter?.firstNamedChild ?? null,
|
|
566
|
+
children[i + 1] ?? null
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (language === 'dart' && type === 'named_argument')
|
|
571
|
+
mark(node.firstNamedChild?.firstNamedChild ?? null, node.lastNamedChild);
|
|
572
|
+
if (
|
|
573
|
+
language === 'kotlin' &&
|
|
574
|
+
type === 'value_argument' &&
|
|
575
|
+
node.children.some((c) => c.type === '=')
|
|
576
|
+
)
|
|
577
|
+
mark(node.firstNamedChild, node.lastNamedChild);
|
|
578
|
+
if (type === 'var_spec' || type === 'const_spec') {
|
|
579
|
+
const names = node.childrenForFieldName('name');
|
|
580
|
+
const values = node.childForFieldName('value')?.namedChildren ?? [];
|
|
581
|
+
names.forEach((name, i) => mark(name, values[i] ?? null));
|
|
582
|
+
} else if (
|
|
583
|
+
language === 'go' &&
|
|
584
|
+
['short_var_declaration', 'assignment_statement'].includes(type)
|
|
585
|
+
) {
|
|
586
|
+
const names = node.childForFieldName('left')?.namedChildren ?? [];
|
|
587
|
+
const values = node.childForFieldName('right')?.namedChildren ?? [];
|
|
588
|
+
names.forEach((name, i) => mark(name, values[i] ?? null));
|
|
589
|
+
} else if (type === 'variable_declarator') {
|
|
590
|
+
mark(
|
|
591
|
+
node.childForFieldName('name'),
|
|
592
|
+
node.childForFieldName('value') ?? node.namedChildren[1] ?? null
|
|
593
|
+
);
|
|
594
|
+
} else if (language === 'php' && type === 'property_element') {
|
|
595
|
+
mark(node.childForFieldName('name'), node.childForFieldName('default_value'));
|
|
596
|
+
} else if (language === 'csharp' && type === 'property_declaration') {
|
|
597
|
+
mark(node.childForFieldName('name'), node.childForFieldName('value'));
|
|
598
|
+
} else if (['val_definition', 'var_definition'].includes(type)) {
|
|
599
|
+
mark(node.childForFieldName('pattern'), node.childForFieldName('value'));
|
|
600
|
+
} else if (
|
|
601
|
+
['assignment', 'assignment_expression', 'operator_assignment'].includes(type) ||
|
|
602
|
+
(language === 'elixir' &&
|
|
603
|
+
type === 'binary_operator' &&
|
|
604
|
+
node.childForFieldName('operator')?.text === '=')
|
|
605
|
+
) {
|
|
606
|
+
mark(node.childForFieldName('left'), node.childForFieldName('right'));
|
|
607
|
+
} else if (['pair', 'keyed_element'].includes(type)) {
|
|
608
|
+
mark(node.childForFieldName('key'), node.childForFieldName('value'));
|
|
609
|
+
} else if (
|
|
610
|
+
language === 'php' &&
|
|
611
|
+
type === 'array_element_initializer' &&
|
|
612
|
+
node.children.some((c) => c.type === '=>')
|
|
613
|
+
) {
|
|
614
|
+
mark(node.namedChildren[0] ?? null, node.namedChildren[1] ?? null);
|
|
615
|
+
} else if (type === 'state_variable_declaration') {
|
|
616
|
+
mark(node.childForFieldName('name'), node.childForFieldName('value'));
|
|
617
|
+
} else if (language === 'solidity' && type === 'variable_declaration_statement') {
|
|
618
|
+
mark(
|
|
619
|
+
node.namedChildren
|
|
620
|
+
.find((c) => c.type === 'variable_declaration')
|
|
621
|
+
?.childForFieldName('name') ?? null,
|
|
622
|
+
node.childForFieldName('value')
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (
|
|
626
|
+
[
|
|
627
|
+
'parameter',
|
|
628
|
+
'optional_parameter',
|
|
629
|
+
'simple_parameter',
|
|
630
|
+
'class_parameter',
|
|
631
|
+
'named_argument',
|
|
632
|
+
].includes(type)
|
|
633
|
+
) {
|
|
634
|
+
const equals = node.children.findIndex((child) => child.type === '=');
|
|
635
|
+
if (equals >= 0)
|
|
636
|
+
mark(
|
|
637
|
+
node.childForFieldName('name') ?? node.firstNamedChild,
|
|
638
|
+
node.children[equals + 1] ?? null
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
let result = '';
|
|
642
|
+
let offset = node.startIndex;
|
|
643
|
+
for (const child of node.namedChildren) {
|
|
644
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
645
|
+
const carry =
|
|
646
|
+
sensitive &&
|
|
647
|
+
(((type !== 'list' || language !== 'clojure') &&
|
|
648
|
+
[
|
|
649
|
+
'expression',
|
|
650
|
+
'expression_list',
|
|
651
|
+
'literal_element',
|
|
652
|
+
'literal_value',
|
|
653
|
+
'array',
|
|
654
|
+
'array_creation_expression',
|
|
655
|
+
'hash',
|
|
656
|
+
'list',
|
|
657
|
+
'keywords',
|
|
658
|
+
'tuple_expression',
|
|
659
|
+
'initializer_expression',
|
|
660
|
+
'collection_expression',
|
|
661
|
+
'parenthesized_expression',
|
|
662
|
+
'unary_expression',
|
|
663
|
+
'interpolated_string_expression',
|
|
664
|
+
'string_literal',
|
|
665
|
+
'heredoc',
|
|
666
|
+
'heredoc_body',
|
|
667
|
+
'nowdoc',
|
|
668
|
+
'regex',
|
|
669
|
+
'quoted_atom',
|
|
670
|
+
'delimited_symbol',
|
|
671
|
+
].includes(type)) ||
|
|
672
|
+
(language === 'clojure' && ['vector', 'hash_map', 'set'].includes(type)) ||
|
|
673
|
+
(language === 'erlang' && type === 'binary' && child.type === 'bin_element') ||
|
|
674
|
+
(language === 'erlang' &&
|
|
675
|
+
type === 'bin_element' &&
|
|
676
|
+
child.id === node.childForFieldName('element')?.id) ||
|
|
677
|
+
(language === 'haskell' &&
|
|
678
|
+
type === 'conditional' &&
|
|
679
|
+
[node.childForFieldName('then')?.id, node.childForFieldName('else')?.id].includes(
|
|
680
|
+
child.id
|
|
681
|
+
)) ||
|
|
682
|
+
(['ocaml', 'ocaml_interface'].includes(language) &&
|
|
683
|
+
((type === 'if_expression' && ['then_clause', 'else_clause'].includes(child.type)) ||
|
|
684
|
+
(['then_clause', 'else_clause'].includes(type) &&
|
|
685
|
+
child.id === node.childForFieldName('expression')?.id))) ||
|
|
686
|
+
(language === 'haskell' && type === 'literal') ||
|
|
687
|
+
(language === 'graphql' &&
|
|
688
|
+
['value', 'list_value', 'object_value', 'object_field'].includes(type)) ||
|
|
689
|
+
(['kotlin', 'dart'].includes(language) &&
|
|
690
|
+
[
|
|
691
|
+
'collection_literal',
|
|
692
|
+
'list_literal',
|
|
693
|
+
'set_or_map_literal',
|
|
694
|
+
'additive_expression',
|
|
695
|
+
'if_null_expression',
|
|
696
|
+
'elvis_expression',
|
|
697
|
+
].includes(type)) ||
|
|
698
|
+
(type === 'composite_literal' && child.id === node.childForFieldName('body')?.id) ||
|
|
699
|
+
(type === 'array_element_initializer' && child.id === node.lastNamedChild?.id) ||
|
|
700
|
+
(['binary_expression', 'binary_operator'].includes(type) &&
|
|
701
|
+
['+', '.', '<>', '||', '??'].includes(
|
|
702
|
+
node.childForFieldName('operator')?.text ?? ''
|
|
703
|
+
)) ||
|
|
704
|
+
(['conditional_expression', 'ternary_expression'].includes(type) &&
|
|
705
|
+
child.id !== node.childForFieldName('condition')?.id) ||
|
|
706
|
+
(['pair', 'keyed_element'].includes(type) &&
|
|
707
|
+
child.id === node.childForFieldName('value')?.id) ||
|
|
708
|
+
([
|
|
709
|
+
'method_invocation',
|
|
710
|
+
'member_access_expression',
|
|
711
|
+
'field_expression',
|
|
712
|
+
'field_access',
|
|
713
|
+
'member_expression',
|
|
714
|
+
].includes(type) &&
|
|
715
|
+
child.id === node.childForFieldName('object')?.id) ||
|
|
716
|
+
(['invocation_expression', 'call_expression', 'call'].includes(type) &&
|
|
717
|
+
[
|
|
718
|
+
node.childForFieldName('function')?.id,
|
|
719
|
+
node.childForFieldName('receiver')?.id,
|
|
720
|
+
].includes(child.id)));
|
|
721
|
+
result += visit(child, carry);
|
|
722
|
+
offset = child.endIndex;
|
|
723
|
+
}
|
|
724
|
+
return result + original.slice(offset - node.startIndex);
|
|
725
|
+
};
|
|
726
|
+
return visit(root);
|
|
727
|
+
}
|
|
728
|
+
|
|
129
729
|
/** CMake substitutions remain source expressions; only AST argument/comment payloads are scrubbed. */
|
|
130
730
|
function redactCmakeNode(node: Node, sensitive = false): string {
|
|
131
731
|
const original = node.text;
|
|
@@ -780,7 +1380,11 @@ function sqlSecretLabel(node: Node | null): boolean {
|
|
|
780
1380
|
return sqlSecretLabel(node.childForFieldName('name') ?? node.firstNamedChild);
|
|
781
1381
|
// This dialect-neutral grammar sometimes calls PostgreSQL quoted identifiers literals.
|
|
782
1382
|
if (node.type === 'identifier' || (node.type === 'literal' && node.text.startsWith('"')))
|
|
783
|
-
return SECRET_LABEL_RE.test(
|
|
1383
|
+
return SECRET_LABEL_RE.test(
|
|
1384
|
+
node.text.startsWith('`')
|
|
1385
|
+
? node.text.slice(1, -1).replaceAll('``', '`')
|
|
1386
|
+
: node.text.replace(/^"|"$/g, '').replaceAll('""', '"')
|
|
1387
|
+
);
|
|
784
1388
|
return false;
|
|
785
1389
|
}
|
|
786
1390
|
|
|
@@ -824,6 +1428,16 @@ function redactSqlNode(node: Node, sensitive = false): string {
|
|
|
824
1428
|
const argumentLabel =
|
|
825
1429
|
node.type === 'function_argument' &&
|
|
826
1430
|
sqlSecretLabel(node.namedChildren.find((child) => child.type === 'identifier') ?? null);
|
|
1431
|
+
const argumentDefaultMarker = argumentLabel
|
|
1432
|
+
? node.children.find((child) => ['keyword_default', '='].includes(child.type))
|
|
1433
|
+
: undefined;
|
|
1434
|
+
const argumentDefaultValue = argumentDefaultMarker
|
|
1435
|
+
? node.namedChildren.find(
|
|
1436
|
+
(child) =>
|
|
1437
|
+
child.startIndex >= argumentDefaultMarker.endIndex &&
|
|
1438
|
+
!['comment', 'marginalia'].includes(child.type)
|
|
1439
|
+
)
|
|
1440
|
+
: undefined;
|
|
827
1441
|
const aliasLabel = node.type === 'term' && sqlSecretLabel(node.childForFieldName('alias'));
|
|
828
1442
|
const insert = node.type === 'list' && node.parent?.type === 'insert' ? node.parent : null;
|
|
829
1443
|
const columns = insert?.namedChildren.find((child) => child.type === 'list');
|
|
@@ -856,7 +1470,7 @@ function redactSqlNode(node: Node, sensitive = false): string {
|
|
|
856
1470
|
(labeled && child.id === right?.id) ||
|
|
857
1471
|
(node.type === 'binary_expression' && sqlSecretLabel(right) && child.id === left?.id) ||
|
|
858
1472
|
(columnLabel && previous?.type === 'keyword_default') ||
|
|
859
|
-
|
|
1473
|
+
child.id === argumentDefaultValue?.id ||
|
|
860
1474
|
(aliasLabel && child.id === node.childForFieldName('value')?.id) ||
|
|
861
1475
|
child.id === node.childForFieldName('password')?.id ||
|
|
862
1476
|
sqlSecretLabel(columnValues[index] ?? null)
|
|
@@ -1375,3 +1989,142 @@ async function scrubVueSource(text: string): Promise<string> {
|
|
|
1375
1989
|
parser.delete();
|
|
1376
1990
|
}
|
|
1377
1991
|
}
|
|
1992
|
+
|
|
1993
|
+
/** Official component offsets delimit payloads; existing JS/TS rules handle script expressions. */
|
|
1994
|
+
async function scrubSvelteSource(text: string): Promise<string> {
|
|
1995
|
+
const marker = '<!-- mnemonik:svelte-source-fidelity-unavailable -->\n';
|
|
1996
|
+
const fallback = (): string => (text.startsWith(marker) ? '' : marker) + scrubPayload(text);
|
|
1997
|
+
if (!text) return text;
|
|
1998
|
+
if (text.includes('mnemonik:svelte-source-fidelity-unavailable')) return fallback();
|
|
1999
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) return fallback();
|
|
2000
|
+
let parser: Parser | undefined;
|
|
2001
|
+
try {
|
|
2002
|
+
const deadline = performance.now() + 4_000;
|
|
2003
|
+
const { parse } = await import('svelte/compiler');
|
|
2004
|
+
const ast = parse(text, { modern: true });
|
|
2005
|
+
const edits: Array<{ start: number; end: number; clean: string }> = [];
|
|
2006
|
+
let faithful = true;
|
|
2007
|
+
parser = new Parser();
|
|
2008
|
+
const js = await loadGrammar('javascript');
|
|
2009
|
+
const ts = await loadGrammar('typescript');
|
|
2010
|
+
if ('unavailable' in js || 'unavailable' in ts) return fallback();
|
|
2011
|
+
parser.setLanguage(js.ok.language);
|
|
2012
|
+
const activeParser = parser;
|
|
2013
|
+
const syntax = (source: string, expression = false, sensitive = false): string => {
|
|
2014
|
+
const prefix = expression ? `const ${sensitive ? 'password' : '__mnemonik_svelte'} = (` : '';
|
|
2015
|
+
const suffix = expression ? '\n);' : '';
|
|
2016
|
+
const full = prefix + source + suffix;
|
|
2017
|
+
const tree = activeParser.parse(full, null, {
|
|
2018
|
+
progressCallback: () => performance.now() > deadline,
|
|
2019
|
+
});
|
|
2020
|
+
try {
|
|
2021
|
+
if (!tree || tree.rootNode.hasError || tree.rootNode.descendantCount > 200_000) {
|
|
2022
|
+
faithful = false;
|
|
2023
|
+
return sensitive ? literalPayload(source, true) : scrubPayload(source);
|
|
2024
|
+
}
|
|
2025
|
+
const clean =
|
|
2026
|
+
full.slice(0, tree.rootNode.startIndex) +
|
|
2027
|
+
redactNode(tree.rootNode) +
|
|
2028
|
+
full.slice(tree.rootNode.endIndex);
|
|
2029
|
+
return clean.slice(prefix.length, clean.length - suffix.length);
|
|
2030
|
+
} finally {
|
|
2031
|
+
tree?.delete();
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
const edit = (start: unknown, end: unknown, clean: (raw: string) => string): void => {
|
|
2035
|
+
if (
|
|
2036
|
+
typeof start !== 'number' ||
|
|
2037
|
+
typeof end !== 'number' ||
|
|
2038
|
+
start < 0 ||
|
|
2039
|
+
end < start ||
|
|
2040
|
+
end > text.length
|
|
2041
|
+
)
|
|
2042
|
+
throw new Error('Component offsets unavailable');
|
|
2043
|
+
edits.push({ start, end, clean: clean(text.slice(start, end)) });
|
|
2044
|
+
};
|
|
2045
|
+
let templateTs = false;
|
|
2046
|
+
for (const script of [ast.module, ast.instance]) {
|
|
2047
|
+
if (!script) continue;
|
|
2048
|
+
const lang = script.attributes.find((a) => a.name === 'lang');
|
|
2049
|
+
const language = !lang
|
|
2050
|
+
? 'js'
|
|
2051
|
+
: Array.isArray(lang.value) && lang.value.length === 1 && lang.value[0]?.type === 'Text'
|
|
2052
|
+
? lang.value[0].data
|
|
2053
|
+
: null;
|
|
2054
|
+
if (!['js', 'ts'].includes(language ?? '') || script.attributes.some((a) => a.name === 'src'))
|
|
2055
|
+
return fallback();
|
|
2056
|
+
parser.setLanguage(language === 'ts' ? ts.ok.language : js.ok.language);
|
|
2057
|
+
const offsets = script.content as unknown as { start?: unknown; end?: unknown };
|
|
2058
|
+
edit(offsets.start, offsets.end, (raw) => syntax(raw));
|
|
2059
|
+
if (script === ast.instance) templateTs = language === 'ts';
|
|
2060
|
+
}
|
|
2061
|
+
parser.setLanguage(templateTs ? ts.ok.language : js.ok.language);
|
|
2062
|
+
const pending: Array<{ value: unknown; sensitive: boolean }> = [
|
|
2063
|
+
{ value: ast.fragment, sensitive: false },
|
|
2064
|
+
];
|
|
2065
|
+
let count = 0;
|
|
2066
|
+
while (pending.length) {
|
|
2067
|
+
if (++count > 200_000 || performance.now() > deadline) return fallback();
|
|
2068
|
+
const entry = pending.pop();
|
|
2069
|
+
if (!entry || !entry.value || typeof entry.value !== 'object') continue;
|
|
2070
|
+
if (Array.isArray(entry.value)) {
|
|
2071
|
+
for (const value of entry.value as unknown[])
|
|
2072
|
+
pending.push({ value, sensitive: entry.sensitive });
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
const node = entry.value as Record<string, unknown>;
|
|
2076
|
+
const type = typeof node.type === 'string' ? node.type : '';
|
|
2077
|
+
if (type === 'Text') {
|
|
2078
|
+
edit(node.start, node.end, (raw) => literalPayload(raw, entry.sensitive));
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (type === 'Comment') {
|
|
2082
|
+
edit(node.start, node.end, (raw) => '<!--' + scrubPayload(raw.slice(4, -3)) + '-->');
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (
|
|
2086
|
+
/Expression$/.test(type) ||
|
|
2087
|
+
['Identifier', 'Literal', 'TemplateLiteral', 'VariableDeclaration'].includes(type)
|
|
2088
|
+
) {
|
|
2089
|
+
edit(node.start, node.end, (raw) =>
|
|
2090
|
+
syntax(raw, type !== 'VariableDeclaration', entry.sensitive)
|
|
2091
|
+
);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const sensitive =
|
|
2095
|
+
entry.sensitive ||
|
|
2096
|
+
(type === 'Attribute' && typeof node.name === 'string' && SECRET_LABEL_RE.test(node.name));
|
|
2097
|
+
for (const [key, value] of Object.entries(node))
|
|
2098
|
+
if (key !== 'parent' && value && typeof value === 'object')
|
|
2099
|
+
pending.push({ value, sensitive });
|
|
2100
|
+
}
|
|
2101
|
+
if (ast.css)
|
|
2102
|
+
edit(ast.css.start, ast.css.end, (raw) => {
|
|
2103
|
+
const clean = scrubPayload(raw);
|
|
2104
|
+
if (clean !== raw) faithful = false;
|
|
2105
|
+
return clean;
|
|
2106
|
+
});
|
|
2107
|
+
// Unparsed tags and boundaries cannot establish fidelity if policy changes them.
|
|
2108
|
+
edits.sort((a, b) => a.start - b.start);
|
|
2109
|
+
let offset = 0;
|
|
2110
|
+
let clean = '';
|
|
2111
|
+
for (const e of edits) {
|
|
2112
|
+
if (e.start < offset) return fallback();
|
|
2113
|
+
const gap = text.slice(offset, e.start);
|
|
2114
|
+
const cleanGap = scrubPayload(gap);
|
|
2115
|
+
if (gap !== cleanGap) faithful = false;
|
|
2116
|
+
clean += cleanGap + e.clean;
|
|
2117
|
+
offset = e.end;
|
|
2118
|
+
}
|
|
2119
|
+
const tail = text.slice(offset);
|
|
2120
|
+
const cleanTail = scrubPayload(tail);
|
|
2121
|
+
if (tail !== cleanTail) faithful = false;
|
|
2122
|
+
clean += cleanTail;
|
|
2123
|
+
parse(clean, { modern: true });
|
|
2124
|
+
return faithful ? clean : marker + clean;
|
|
2125
|
+
} catch {
|
|
2126
|
+
return fallback();
|
|
2127
|
+
} finally {
|
|
2128
|
+
parser?.delete();
|
|
2129
|
+
}
|
|
2130
|
+
}
|