@mnemonik/shared 7.140.3 → 7.141.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/codeScanner.d.ts +1 -1
- package/dist/codeScanner.d.ts.map +1 -1
- package/dist/codeScanner.js +14 -11
- package/dist/codeScanner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/redactCredentials.d.ts +28 -0
- package/dist/redactCredentials.d.ts.map +1 -0
- package/dist/redactCredentials.js +491 -0
- package/dist/redactCredentials.js.map +1 -0
- package/dist/secretPatterns.d.ts +3 -0
- package/dist/secretPatterns.d.ts.map +1 -1
- package/dist/secretPatterns.js +85 -6
- package/dist/secretPatterns.js.map +1 -1
- package/dist/sourceRedaction.d.ts +3 -0
- package/dist/sourceRedaction.d.ts.map +1 -0
- package/dist/sourceRedaction.js +143 -0
- package/dist/sourceRedaction.js.map +1 -0
- package/package.json +1 -1
- package/src/codeScanner.ts +20 -12
- package/src/index.ts +6 -0
- package/src/redactCredentials.ts +594 -0
- package/src/secretPatterns.ts +89 -8
- package/src/sourceRedaction.ts +149 -0
package/src/secretPatterns.ts
CHANGED
|
@@ -42,6 +42,86 @@
|
|
|
42
42
|
|
|
43
43
|
export const SECRET_REDACTION_PLACEHOLDER = '[REDACTED]';
|
|
44
44
|
|
|
45
|
+
const SECRET_LABEL = String.raw`[\w$-]*(?:api[_-]?key|secret|token|password|credential|auth)|aws_secret_access_key`;
|
|
46
|
+
export const SECRET_LABEL_RE = new RegExp(`^(?:${SECRET_LABEL}|MYSQL_PWD|SSHPASS)$`, 'i');
|
|
47
|
+
const LABELED_VALUE_RE = new RegExp(
|
|
48
|
+
String.raw`(?<![\w$-])((["']?)(?:${SECRET_LABEL})\2\s*[:=]\s*)(?=\S)`,
|
|
49
|
+
'gi'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** Keep physical and escaped newline sequences, including JS line continuations. */
|
|
53
|
+
export function redactSecretValue(value: string): string {
|
|
54
|
+
return (
|
|
55
|
+
SECRET_REDACTION_PLACEHOLDER +
|
|
56
|
+
(value.match(/\\(?:\r\n|[\r\n])|\r\n|[\r\n]|\\[rn]/g)?.join('') ?? '')
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function redactLabeledValues(text: string): string {
|
|
61
|
+
let result = '';
|
|
62
|
+
let offset = 0;
|
|
63
|
+
LABELED_VALUE_RE.lastIndex = 0;
|
|
64
|
+
for (const match of text.matchAll(LABELED_VALUE_RE)) {
|
|
65
|
+
if (match.index < offset) continue;
|
|
66
|
+
const start = match.index + match[0].length;
|
|
67
|
+
const escaped = text[start] === '\\' && /^["']/.test(text.charAt(start + 1));
|
|
68
|
+
const quote = escaped
|
|
69
|
+
? text.charAt(start + 1)
|
|
70
|
+
: /^["'`]/.test(text.charAt(start))
|
|
71
|
+
? text.charAt(start)
|
|
72
|
+
: '';
|
|
73
|
+
const delimiter = (escaped ? '\\' : '') + quote;
|
|
74
|
+
let end = start;
|
|
75
|
+
let closed = false;
|
|
76
|
+
if (quote) {
|
|
77
|
+
end += delimiter.length;
|
|
78
|
+
while (end < text.length) {
|
|
79
|
+
if (text[end] === '\\') {
|
|
80
|
+
if (escaped) {
|
|
81
|
+
const begin = end;
|
|
82
|
+
while (text[end] === '\\') end++;
|
|
83
|
+
// A quoted value embedded in a string: \" closes, \\\" escapes its quote.
|
|
84
|
+
if (text[end] === quote && (end - begin) % 4 === 1) {
|
|
85
|
+
end++;
|
|
86
|
+
closed = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
end = Math.min(end + 1, text.length);
|
|
90
|
+
} else {
|
|
91
|
+
end = Math.min(end + 2, text.length);
|
|
92
|
+
}
|
|
93
|
+
} else if (text[end++] === quote && !escaped) {
|
|
94
|
+
closed = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Never split an escape pair and leave its quote as executable syntax.
|
|
100
|
+
end += /^(?:\\[^\r\n]|[^\s"'`,;()[\]{}\\])+/.exec(text.slice(start))?.[0].length ?? 0;
|
|
101
|
+
}
|
|
102
|
+
const value = text.slice(start, end);
|
|
103
|
+
if (!/[A-Za-z0-9]/.test(value) || ENV_READ_VALUE_RE.test(match[0] + value)) continue;
|
|
104
|
+
// The quote closes a command string or anchored label search, such as '^API_KEY='.
|
|
105
|
+
if (
|
|
106
|
+
quote &&
|
|
107
|
+
!escaped &&
|
|
108
|
+
(text[match.index - 1] === quote ||
|
|
109
|
+
(text[match.index - 1] === '^' &&
|
|
110
|
+
text[match.index - 2] === quote &&
|
|
111
|
+
(start + 1 === text.length || /\s/.test(text.charAt(start + 1)))))
|
|
112
|
+
)
|
|
113
|
+
continue;
|
|
114
|
+
if (/aws_secret_access_key/i.test(match[0]) && /^<[^>]*>$/.test(value)) continue;
|
|
115
|
+
const body = quote
|
|
116
|
+
? value.slice(delimiter.length, closed ? -delimiter.length : undefined)
|
|
117
|
+
: value.replace(/(?:\*\/|[.!?]+)$/, '');
|
|
118
|
+
const tail = quote ? (closed ? delimiter : '') : value.slice(body.length);
|
|
119
|
+
result += text.slice(offset, start) + delimiter + redactSecretValue(body) + tail;
|
|
120
|
+
offset = end;
|
|
121
|
+
}
|
|
122
|
+
return result + text.slice(offset);
|
|
123
|
+
}
|
|
124
|
+
|
|
45
125
|
// Shared by the scanner daemon and server-side scrub paths.
|
|
46
126
|
//
|
|
47
127
|
// A value must hold a letter or a digit, in every pattern whose value is free
|
|
@@ -50,7 +130,7 @@ export const SECRET_REDACTION_PLACEHOLDER = '[REDACTED]';
|
|
|
50
130
|
// flag: "--password=; repeated copies" in a stored checkpoint became
|
|
51
131
|
// "--[REDACTED] repeated copies" (2026-10-13), and so did "password: ***".
|
|
52
132
|
export const SECRET_PATTERNS: ReadonlyArray<RegExp> = [
|
|
53
|
-
|
|
133
|
+
LABELED_VALUE_RE, // Select the label; the linear reader consumes its complete value.
|
|
54
134
|
// Stripe-shape: (sk|pk)_(live|test)_<24+ alphanumerics>. Catches modern
|
|
55
135
|
// Stripe keys whose body is split by an environment separator that
|
|
56
136
|
// breaks the contiguous-alphanum pattern below. Required `live|test`
|
|
@@ -71,10 +151,7 @@ export const SECRET_PATTERNS: ReadonlyArray<RegExp> = [
|
|
|
71
151
|
// \b-delimited, so prose ("AKIAMIA is a place name") cannot match.
|
|
72
152
|
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
73
153
|
/\bASIA[0-9A-Z]{16}\b/g,
|
|
74
|
-
// AWS secret access
|
|
75
|
-
// NOT cover this: its `secret` alternative must be followed immediately by
|
|
76
|
-
// `[:=]`, and here it is followed by `_access_key`.
|
|
77
|
-
/\baws_secret_access_key\s*[:=]\s*[A-Za-z0-9/+=]{40}\b/gi,
|
|
154
|
+
// Labeled AWS secret access keys are covered by LABELED_VALUE_RE, including quotes.
|
|
78
155
|
// JWT: three base64url segments. Length minimums keep dotted identifiers
|
|
79
156
|
// (`payload.header.signature`) out.
|
|
80
157
|
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g,
|
|
@@ -253,9 +330,13 @@ export function scrubSecrets(text: string): string {
|
|
|
253
330
|
if (!text) return text;
|
|
254
331
|
let result = text;
|
|
255
332
|
for (const pattern of SECRET_PATTERNS) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
333
|
+
if (pattern === LABELED_VALUE_RE) {
|
|
334
|
+
result = redactLabeledValues(result);
|
|
335
|
+
} else {
|
|
336
|
+
result = result.replace(pattern, (match) =>
|
|
337
|
+
ENV_READ_VALUE_RE.test(match) ? match : redactSecretValue(match)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
259
340
|
}
|
|
260
341
|
// Backstop for credentials with no recognizable prefix. Runs last so the
|
|
261
342
|
// precise provider patterns get first claim on their own shapes.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { Parser, type Node, type Tree } from 'web-tree-sitter';
|
|
3
|
+
import { loadGrammar } from './ast/grammars.js';
|
|
4
|
+
import { MAX_AST_PARSE_BYTES } from './ast/astChunker.js';
|
|
5
|
+
import { redactCredentials } from './redactCredentials.js';
|
|
6
|
+
import {
|
|
7
|
+
redactHighEntropyTokens,
|
|
8
|
+
redactSecretValue,
|
|
9
|
+
scrubSecrets,
|
|
10
|
+
SECRET_LABEL_RE,
|
|
11
|
+
} from './secretPatterns.js';
|
|
12
|
+
|
|
13
|
+
function scrubPayload(text: string): string {
|
|
14
|
+
return redactCredentials(scrubSecrets(text), { commandLineOnly: true }).text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Source syntax stays executable; unsupported or incomplete input uses the conservative scrubber. */
|
|
18
|
+
export async function scrubSourceSecrets(text: string, filePath: string): Promise<string> {
|
|
19
|
+
const extension = extname(filePath).toLowerCase();
|
|
20
|
+
const language = ['.ts', '.mts', '.cts'].includes(extension)
|
|
21
|
+
? 'typescript'
|
|
22
|
+
: extension === '.tsx'
|
|
23
|
+
? 'tsx'
|
|
24
|
+
: ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
|
|
25
|
+
? 'javascript'
|
|
26
|
+
: null;
|
|
27
|
+
if (!text || !language || Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) {
|
|
28
|
+
return scrubPayload(text);
|
|
29
|
+
}
|
|
30
|
+
let parser: Parser | undefined;
|
|
31
|
+
let tree: Tree | null = null;
|
|
32
|
+
try {
|
|
33
|
+
const grammar = await loadGrammar(language);
|
|
34
|
+
if ('unavailable' in grammar) return scrubPayload(text);
|
|
35
|
+
parser = new Parser();
|
|
36
|
+
parser.setLanguage(grammar.ok.language);
|
|
37
|
+
tree = parser.parse(text);
|
|
38
|
+
if (!tree || tree.rootNode.hasError) return scrubPayload(text);
|
|
39
|
+
const root = tree.rootNode;
|
|
40
|
+
return text.slice(0, root.startIndex) + redactNode(root) + text.slice(root.endIndex);
|
|
41
|
+
} catch {
|
|
42
|
+
return scrubPayload(text);
|
|
43
|
+
} finally {
|
|
44
|
+
tree?.delete();
|
|
45
|
+
parser?.delete();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function secretLabel(node: Node | null): boolean {
|
|
50
|
+
if (!node) return false;
|
|
51
|
+
const property = node.childForFieldName('property') ?? node;
|
|
52
|
+
const name = property.type === 'string' ? property.text.slice(1, -1) : property.text;
|
|
53
|
+
return SECRET_LABEL_RE.test(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function literalPayload(text: string, sensitive: boolean): string {
|
|
57
|
+
if (sensitive && /[A-Za-z0-9]/.test(text)) return redactSecretValue(text);
|
|
58
|
+
// Only the entropy backstop needs an opening quote; labeled values must not consume it.
|
|
59
|
+
return redactHighEntropyTokens('"' + scrubPayload(text) + '"').slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function redactNode(node: Node, sensitive = false): string {
|
|
63
|
+
const original = node.text;
|
|
64
|
+
if (node.type === 'comment') {
|
|
65
|
+
return original.startsWith('/*')
|
|
66
|
+
? '/*' + scrubPayload(original.slice(2, -2)) + '*/'
|
|
67
|
+
: '//' + scrubPayload(original.slice(2));
|
|
68
|
+
}
|
|
69
|
+
if (node.type === 'string') {
|
|
70
|
+
return original[0] + literalPayload(original.slice(1, -1), sensitive) + original.at(-1);
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
node.type === 'property_identifier' &&
|
|
74
|
+
node.parent?.type === 'pair' &&
|
|
75
|
+
node.parent.childForFieldName('key')?.id === node.id
|
|
76
|
+
) {
|
|
77
|
+
const payload = literalPayload(original, false);
|
|
78
|
+
return payload === original ? original : '"' + payload + '"';
|
|
79
|
+
}
|
|
80
|
+
if (node.type === 'number' && sensitive) return `'${redactSecretValue(original)}'`;
|
|
81
|
+
if (node.type === 'template_string') {
|
|
82
|
+
let result = '`';
|
|
83
|
+
let offset = node.startIndex + 1;
|
|
84
|
+
for (const child of node.namedChildren) {
|
|
85
|
+
if (child.type !== 'template_substitution') continue;
|
|
86
|
+
result += literalPayload(
|
|
87
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex),
|
|
88
|
+
sensitive
|
|
89
|
+
);
|
|
90
|
+
result += redactNode(child);
|
|
91
|
+
offset = child.endIndex;
|
|
92
|
+
}
|
|
93
|
+
return result + literalPayload(original.slice(offset - node.startIndex, -1), sensitive) + '`';
|
|
94
|
+
}
|
|
95
|
+
if (node.type === 'regex_pattern' || node.type === 'jsx_text') return scrubPayload(original);
|
|
96
|
+
// A bare `token: value` fragment is also a valid JS labeled statement.
|
|
97
|
+
if (node.type === 'labeled_statement' && secretLabel(node.childForFieldName('label'))) {
|
|
98
|
+
const body = node.childForFieldName('body');
|
|
99
|
+
if (body?.firstNamedChild?.type === 'identifier') return scrubPayload(original);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let value: Node | null = null;
|
|
103
|
+
let key: Node | null = null;
|
|
104
|
+
if (
|
|
105
|
+
[
|
|
106
|
+
'variable_declarator',
|
|
107
|
+
'pair',
|
|
108
|
+
'public_field_definition',
|
|
109
|
+
'assignment_pattern',
|
|
110
|
+
'required_parameter',
|
|
111
|
+
].includes(node.type)
|
|
112
|
+
) {
|
|
113
|
+
key =
|
|
114
|
+
node.childForFieldName('name') ??
|
|
115
|
+
node.childForFieldName('key') ??
|
|
116
|
+
node.childForFieldName('pattern') ??
|
|
117
|
+
node.childForFieldName('left');
|
|
118
|
+
value = node.childForFieldName('value') ?? node.childForFieldName('right');
|
|
119
|
+
} else if (node.type === 'assignment_expression') {
|
|
120
|
+
key = node.childForFieldName('left');
|
|
121
|
+
value = node.childForFieldName('right');
|
|
122
|
+
} else if (node.type === 'jsx_attribute') {
|
|
123
|
+
[key = null, value = null] = node.namedChildren;
|
|
124
|
+
}
|
|
125
|
+
const labeled = secretLabel(key);
|
|
126
|
+
let result = '';
|
|
127
|
+
let offset = node.startIndex;
|
|
128
|
+
for (const child of node.namedChildren) {
|
|
129
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
130
|
+
// Carry labels through value wrappers, never through call arguments or conditions.
|
|
131
|
+
const carry =
|
|
132
|
+
sensitive &&
|
|
133
|
+
(([
|
|
134
|
+
'parenthesized_expression',
|
|
135
|
+
'as_expression',
|
|
136
|
+
'satisfies_expression',
|
|
137
|
+
'non_null_expression',
|
|
138
|
+
].includes(node.type) &&
|
|
139
|
+
child.id === node.firstNamedChild?.id) ||
|
|
140
|
+
(node.type === 'type_assertion' && child.id === node.lastNamedChild?.id) ||
|
|
141
|
+
(node.type === 'binary_expression' &&
|
|
142
|
+
['+', '||', '&&', '??'].includes(node.childForFieldName('operator')?.text ?? '')) ||
|
|
143
|
+
(node.type === 'ternary_expression' &&
|
|
144
|
+
child.id !== node.childForFieldName('condition')?.id));
|
|
145
|
+
result += redactNode(child, carry || (labeled && child.id === value?.id));
|
|
146
|
+
offset = child.endIndex;
|
|
147
|
+
}
|
|
148
|
+
return result + original.slice(offset - node.startIndex);
|
|
149
|
+
}
|