@iris-code/core 0.1.2 → 0.3.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/dist/config.d.ts +14 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +48 -5
- package/dist/config.js.map +1 -1
- package/dist/fileNaming.d.ts +1 -0
- package/dist/fileNaming.d.ts.map +1 -1
- package/dist/fileNaming.js +29 -0
- package/dist/fileNaming.js.map +1 -1
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +44 -2
- package/dist/guards.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -9
- package/dist/index.js.map +1 -1
- package/dist/java/analyser.d.ts +4 -0
- package/dist/java/analyser.d.ts.map +1 -0
- package/dist/java/analyser.js +394 -0
- package/dist/java/analyser.js.map +1 -0
- package/dist/java/lexer.d.ts +46 -0
- package/dist/java/lexer.d.ts.map +1 -0
- package/dist/java/lexer.js +198 -0
- package/dist/java/lexer.js.map +1 -0
- package/dist/languages.d.ts +2 -10
- package/dist/languages.d.ts.map +1 -1
- package/dist/languages.js +18 -35
- package/dist/languages.js.map +1 -1
- package/dist/registry.d.ts +39 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +74 -0
- package/dist/registry.js.map +1 -0
- package/dist/rust/analyser.d.ts +26 -0
- package/dist/rust/analyser.d.ts.map +1 -0
- package/dist/rust/analyser.js +506 -0
- package/dist/rust/analyser.js.map +1 -0
- package/dist/rust/lexer.d.ts +39 -0
- package/dist/rust/lexer.d.ts.map +1 -0
- package/dist/rust/lexer.js +192 -0
- package/dist/rust/lexer.js.map +1 -0
- package/dist/secrets.d.ts.map +1 -1
- package/dist/secrets.js +357 -1
- package/dist/secrets.js.map +1 -1
- package/dist/sfc.d.ts.map +1 -1
- package/dist/sfc.js +4 -18
- package/dist/sfc.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The Rust lexical layer, in its own module because TWO callers need it: the
|
|
4
|
+
* Rust analyser (every structural rule) and `detectParseErrorReason` in
|
|
5
|
+
* guards.ts (the delimiter balance check that decides whether a file is
|
|
6
|
+
* analysed at all).
|
|
7
|
+
*
|
|
8
|
+
* That second caller is why this exists before any rule. C# shipped a defect
|
|
9
|
+
* where a verbatim string read as unterminated, every brace after it landed at
|
|
10
|
+
* the wrong depth, and the file came back a parse error with every finding
|
|
11
|
+
* zeroed - and a zeroed file is indistinguishable from a clean one.
|
|
12
|
+
*
|
|
13
|
+
* Both modes are LENGTH- and LINE-PRESERVING, so a finding's offset still maps
|
|
14
|
+
* to its native line with no remapping layer that could drift.
|
|
15
|
+
*
|
|
16
|
+
* THE LIFETIME IS THE RUST-SPECIFIC TRAP, and it is the exact shape that breaks
|
|
17
|
+
* a naive scanner. `&'a str` is a reference with a lifetime, not a char literal:
|
|
18
|
+
* a scanner that treats `'` as opening a character would run to the NEXT
|
|
19
|
+
* apostrophe somewhere further down the file, blanking real code in between.
|
|
20
|
+
* Lifetimes appear in most non-trivial Rust, so this is the common case rather
|
|
21
|
+
* than an edge one.
|
|
22
|
+
*
|
|
23
|
+
* Also handled, each with its own way of breaking a naive scan:
|
|
24
|
+
* - raw strings at any hash depth: `r"..."`, `r#"..."#`, `r##"..."##`, where a
|
|
25
|
+
* `"` inside does NOT close the literal
|
|
26
|
+
* - raw identifiers: `r#type` is an identifier, not a raw string, so `r#` only
|
|
27
|
+
* opens a literal when a quote follows the hashes
|
|
28
|
+
* - byte strings `b"..."` and byte chars `b'x'`, plus `br#"..."#`
|
|
29
|
+
* - NESTED block comments, which Rust allows and C does not. An inner block
|
|
30
|
+
* comment's closer does NOT end the outer one, so a depth counter is the only
|
|
31
|
+
* way to see where the comment really ends. (Writing that construct literally
|
|
32
|
+
* in this very comment ended it early on the first build - the same class of
|
|
33
|
+
* mistake the module exists to prevent.)
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.transformRust = transformRust;
|
|
37
|
+
exports.stripRustComments = stripRustComments;
|
|
38
|
+
exports.rustCodeOnly = rustCodeOnly;
|
|
39
|
+
/** True when the `'` at `index` opens a char literal rather than a lifetime. */
|
|
40
|
+
function isCharLiteral(source, index) {
|
|
41
|
+
// `'\n'`, `'\''`, `'\u{1F600}'`: an escape always means a char literal.
|
|
42
|
+
if (source[index + 1] === '\\')
|
|
43
|
+
return true;
|
|
44
|
+
// `'a'` is a char; `'a` followed by anything else is a lifetime. A char
|
|
45
|
+
// literal's body is a single scalar, so the quote sits two positions along.
|
|
46
|
+
if (source[index + 2] === "'")
|
|
47
|
+
return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function transformRust(source, blankStrings) {
|
|
51
|
+
let result = '';
|
|
52
|
+
let i = 0;
|
|
53
|
+
const blank = (ch) => (ch === '\n' || ch === '\r' ? ch : ' ');
|
|
54
|
+
const emit = (ch) => { result += blankStrings ? blank(ch) : ch; };
|
|
55
|
+
while (i < source.length) {
|
|
56
|
+
const ch = source[i];
|
|
57
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
58
|
+
while (i < source.length && source[i] !== '\n') {
|
|
59
|
+
result += blank(source[i]);
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Nested block comments. A single `*/` does not necessarily end the comment.
|
|
65
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
66
|
+
let depth = 0;
|
|
67
|
+
while (i < source.length) {
|
|
68
|
+
if (source[i] === '/' && source[i + 1] === '*') {
|
|
69
|
+
depth++;
|
|
70
|
+
result += ' ';
|
|
71
|
+
i += 2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (source[i] === '*' && source[i + 1] === '/') {
|
|
75
|
+
depth--;
|
|
76
|
+
result += ' ';
|
|
77
|
+
i += 2;
|
|
78
|
+
if (depth === 0)
|
|
79
|
+
break;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
result += blank(source[i]);
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Raw and byte string prefixes: r"..", r#".."#, b"..", br#".."#.
|
|
88
|
+
if (ch === 'r' || ch === 'b') {
|
|
89
|
+
let j = i;
|
|
90
|
+
if (source[j] === 'b')
|
|
91
|
+
j++;
|
|
92
|
+
if (source[j] === 'r') {
|
|
93
|
+
j++;
|
|
94
|
+
let hashes = 0;
|
|
95
|
+
while (source[j] === '#') {
|
|
96
|
+
hashes++;
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
if (source[j] === '"') {
|
|
100
|
+
// A raw string. Its terminator is a quote followed by exactly the
|
|
101
|
+
// opening number of hashes, so an inner `"` cannot close it.
|
|
102
|
+
const closer = '"' + '#'.repeat(hashes);
|
|
103
|
+
for (let k = i; k <= j; k++)
|
|
104
|
+
emit(source[k]);
|
|
105
|
+
i = j + 1;
|
|
106
|
+
while (i < source.length) {
|
|
107
|
+
if (source.startsWith(closer, i)) {
|
|
108
|
+
for (let k = 0; k < closer.length; k++)
|
|
109
|
+
emit(source[i + k]);
|
|
110
|
+
i += closer.length;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
emit(source[i]);
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// `r#type`: a raw identifier, not a literal. Fall through as code.
|
|
119
|
+
}
|
|
120
|
+
else if (source[i] === 'b' && (source[j] === '"' || source[j] === "'")) {
|
|
121
|
+
// Byte string or byte char: emit the prefix, then handle as usual.
|
|
122
|
+
result += 'b';
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (ch === '"') {
|
|
128
|
+
emit(ch);
|
|
129
|
+
i++;
|
|
130
|
+
while (i < source.length) {
|
|
131
|
+
const current = source[i];
|
|
132
|
+
if (current === '\\') {
|
|
133
|
+
emit(current);
|
|
134
|
+
i++;
|
|
135
|
+
if (i < source.length) {
|
|
136
|
+
emit(source[i]);
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
emit(current);
|
|
142
|
+
i++;
|
|
143
|
+
// Rust strings may span lines, so a newline does not terminate one.
|
|
144
|
+
if (current === '"')
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ch === "'") {
|
|
150
|
+
if (!isCharLiteral(source, i)) {
|
|
151
|
+
// A lifetime. Emit the apostrophe as code and carry on; treating it as
|
|
152
|
+
// a literal would blank everything up to the next apostrophe.
|
|
153
|
+
result += ch;
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
emit(ch);
|
|
158
|
+
i++;
|
|
159
|
+
while (i < source.length) {
|
|
160
|
+
const current = source[i];
|
|
161
|
+
if (current === '\n')
|
|
162
|
+
break;
|
|
163
|
+
if (current === '\\') {
|
|
164
|
+
emit(current);
|
|
165
|
+
i++;
|
|
166
|
+
if (i < source.length) {
|
|
167
|
+
emit(source[i]);
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
emit(current);
|
|
173
|
+
i++;
|
|
174
|
+
if (current === "'")
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
result += ch;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
/** Comments blanked, string contents intact. */
|
|
185
|
+
function stripRustComments(source) {
|
|
186
|
+
return transformRust(source, false);
|
|
187
|
+
}
|
|
188
|
+
/** Comments AND string contents blanked: the input for anything structural. */
|
|
189
|
+
function rustCodeOnly(source) {
|
|
190
|
+
return transformRust(source, true);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=lexer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/rust/lexer.ts"],"names":[],"mappings":"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"}
|
package/dist/secrets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAMnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO1D;
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAMnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO1D;AA8hBD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAK,GACZ,eAAe,EAAE,CA6BnB"}
|
package/dist/secrets.js
CHANGED
|
@@ -53,6 +53,8 @@ function detectLayer1Ts(source) {
|
|
|
53
53
|
continue;
|
|
54
54
|
if (isPlaceholder(value, varName))
|
|
55
55
|
continue;
|
|
56
|
+
if (valueIsNotACredential(value, varName))
|
|
57
|
+
continue;
|
|
56
58
|
results.push({
|
|
57
59
|
line: lineOf(source, m.index),
|
|
58
60
|
variable: varName,
|
|
@@ -79,6 +81,8 @@ function detectLayer1Go(source) {
|
|
|
79
81
|
continue;
|
|
80
82
|
if (isPlaceholder(value, varName))
|
|
81
83
|
continue;
|
|
84
|
+
if (valueIsNotACredential(value, varName))
|
|
85
|
+
continue;
|
|
82
86
|
results.push({
|
|
83
87
|
line: lineOf(source, m.index),
|
|
84
88
|
variable: varName,
|
|
@@ -99,6 +103,8 @@ function detectLayer1Go(source) {
|
|
|
99
103
|
continue;
|
|
100
104
|
if (isPlaceholder(value, varName))
|
|
101
105
|
continue;
|
|
106
|
+
if (valueIsNotACredential(value, varName))
|
|
107
|
+
continue;
|
|
102
108
|
results.push({
|
|
103
109
|
line: lineOf(source, m.index),
|
|
104
110
|
variable: varName,
|
|
@@ -124,6 +130,8 @@ function detectLayer1Py(source) {
|
|
|
124
130
|
continue;
|
|
125
131
|
if (isPlaceholder(value, varName))
|
|
126
132
|
continue;
|
|
133
|
+
if (valueIsNotACredential(value, varName))
|
|
134
|
+
continue;
|
|
127
135
|
results.push({
|
|
128
136
|
line: lineOf(source, m.index),
|
|
129
137
|
variable: varName,
|
|
@@ -145,6 +153,8 @@ function detectLayer1Ruby(source) {
|
|
|
145
153
|
const value = m[4] ?? m[5];
|
|
146
154
|
if (!varName || !value || value.length < 8 || !hasSuspectKeyword(varName) || isPlaceholder(value, varName))
|
|
147
155
|
continue;
|
|
156
|
+
if (valueIsNotACredential(value, varName))
|
|
157
|
+
continue;
|
|
148
158
|
results.push({ line: lineOf(source, m.index), variable: varName, pattern: 'suspicious-name', message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`, value, maskedValue: maskSecret(value) });
|
|
149
159
|
}
|
|
150
160
|
return results;
|
|
@@ -169,6 +179,8 @@ function detectLayer1CSharp(source) {
|
|
|
169
179
|
const value = m[2] ?? m[3];
|
|
170
180
|
if (!varName || !value || value.length < 8 || !hasSuspectKeyword(varName) || isPlaceholder(value, varName))
|
|
171
181
|
continue;
|
|
182
|
+
if (valueIsNotACredential(value, varName))
|
|
183
|
+
continue;
|
|
172
184
|
results.push({ line: lineOf(source, m.index), variable: varName, pattern: 'suspicious-name', message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`, value, maskedValue: maskSecret(value) });
|
|
173
185
|
}
|
|
174
186
|
const xmlRe = /<([A-Za-z_][\w.-]*(?:Key|Token|Secret|Password|Credential|Private|Pwd)[\w.-]*)>([^<]{8,500})<\/\1>/gi;
|
|
@@ -177,6 +189,8 @@ function detectLayer1CSharp(source) {
|
|
|
177
189
|
const value = m[2].trim();
|
|
178
190
|
if (!value || CSHARP_XML_EXEMPT_PROPERTIES.has(varName.toLowerCase()) || isPlaceholder(value, varName))
|
|
179
191
|
continue;
|
|
192
|
+
if (valueIsNotACredential(value, varName))
|
|
193
|
+
continue;
|
|
180
194
|
// A value made only of MSBuild property/item references - $(ApiToken),
|
|
181
195
|
// @(Items), %(Meta) - is injected at build time from CI or a secret store.
|
|
182
196
|
// That is the FIX for this rule, so flagging it would punish it.
|
|
@@ -186,6 +200,342 @@ function detectLayer1CSharp(source) {
|
|
|
186
200
|
}
|
|
187
201
|
return results;
|
|
188
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A value that NAMES a credential field rather than holding one.
|
|
205
|
+
*
|
|
206
|
+
* `private static final String PASSWORD_FIELD = "password"` and
|
|
207
|
+
* `API_KEY_HEADER = "X-API-Key"` are constants declaring a key name, and they
|
|
208
|
+
* are everywhere in Java config and HTTP code. Both halves carry a suspect
|
|
209
|
+
* keyword, so the variable-name heuristic alone reports every one of them.
|
|
210
|
+
*
|
|
211
|
+
* Three conditions together, and all three are needed. The suspect keyword alone
|
|
212
|
+
* is far too loose: `an0therPr0dSecret` is a plausible password that contains
|
|
213
|
+
* the segment "secret", and an earlier version of this check silently swallowed
|
|
214
|
+
* it - trading a false positive for a false NEGATIVE on the rule that matters
|
|
215
|
+
* most here.
|
|
216
|
+
*
|
|
217
|
+
* - NO DIGITS. A field name is `password` or `X-API-Key`; a credential almost
|
|
218
|
+
* always carries at least one digit. This is the strongest of the three.
|
|
219
|
+
* - SHORT. Field names are 8 to 16 characters; credentials are longer.
|
|
220
|
+
* - The value is a single unspaced identifier or header token that is ITSELF a
|
|
221
|
+
* suspect keyword.
|
|
222
|
+
*
|
|
223
|
+
* Accepted edge: a genuine credential that is short, digit-free AND contains a
|
|
224
|
+
* credential word - `secretpass` - is missed. A value that weak is a placeholder
|
|
225
|
+
* far more often than a live key, and Layer 2 still covers anything with a
|
|
226
|
+
* recognisable provider format.
|
|
227
|
+
*
|
|
228
|
+
* Scoped to the detectors added with it rather than folded into `isPlaceholder`,
|
|
229
|
+
* so the existing per-language benchmark baselines cannot shift underneath a
|
|
230
|
+
* change that was about Java. Worth promoting once it has a baseline of its own.
|
|
231
|
+
*/
|
|
232
|
+
function namesAFieldRatherThanHoldingOne(value) {
|
|
233
|
+
if (value.length > 16)
|
|
234
|
+
return false;
|
|
235
|
+
if (!/^[A-Za-z][A-Za-z._-]*$/.test(value))
|
|
236
|
+
return false;
|
|
237
|
+
return hasSuspectKeyword(value);
|
|
238
|
+
}
|
|
239
|
+
/** Longest unbroken run of letters and digits. */
|
|
240
|
+
function longestAlphanumericRun(value) {
|
|
241
|
+
let best = '';
|
|
242
|
+
let run = '';
|
|
243
|
+
for (const ch of value) {
|
|
244
|
+
if (/[A-Za-z0-9]/.test(ch)) {
|
|
245
|
+
run += ch;
|
|
246
|
+
if (run.length > best.length)
|
|
247
|
+
best = run;
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
run = '';
|
|
251
|
+
}
|
|
252
|
+
return best;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* True when the value contains a run that looks like a generated credential.
|
|
256
|
+
*
|
|
257
|
+
* Length alone is not the signal, which the first version of this got wrong: it
|
|
258
|
+
* treated any 12-character run as credential-shaped, and `authentication` is 14
|
|
259
|
+
* letters of ordinary English. That single mistake kept `ACKNOWLEDGED_KEY =
|
|
260
|
+
* "acknowledged"` and a log message containing the word "authentication" in the
|
|
261
|
+
* results, because the gate refused to let any other rule exempt them.
|
|
262
|
+
*
|
|
263
|
+
* A generated credential MIXES letters and digits, or is long enough that no
|
|
264
|
+
* English word explains it. Both forms are checked against the longest run rather
|
|
265
|
+
* than the whole value, so separators cannot dilute the signal.
|
|
266
|
+
*/
|
|
267
|
+
function looksCredentialShaped(value) {
|
|
268
|
+
const run = longestAlphanumericRun(value);
|
|
269
|
+
if (run.length < 12)
|
|
270
|
+
return false;
|
|
271
|
+
return (/[0-9]/.test(run) && /[A-Za-z]/.test(run)) || run.length >= 20;
|
|
272
|
+
}
|
|
273
|
+
/** Letters and digits only, lowercased, so two spellings of one name compare equal. */
|
|
274
|
+
function alphanumericLower(value) {
|
|
275
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True when the value is the variable's OWN NAME rather than a credential.
|
|
279
|
+
*
|
|
280
|
+
* This is the single most common false positive on real Java, and the shape is
|
|
281
|
+
* unmistakable once seen: a constant that declares a settings key or a JSON field
|
|
282
|
+
* has a value that mirrors its own identifier.
|
|
283
|
+
*
|
|
284
|
+
* static final String PATTERNS_KEY = "patterns";
|
|
285
|
+
* static final String CLUSTER_UUID_KEY = "cluster_uuid";
|
|
286
|
+
* static final String HIDDEN_FIELD_KEY = "hidden_field";
|
|
287
|
+
* static final String SERVICE_ACCOUNT_TOKEN_DOC_TYPE = "service_account_token";
|
|
288
|
+
*
|
|
289
|
+
* Compared after stripping separators and case, and after removing the credential
|
|
290
|
+
* nouns from the NAME - which is what makes `PATTERNS_KEY` reduce to `patterns`.
|
|
291
|
+
* `isPlaceholder` already covers exact equality; this covers the suffixed forms.
|
|
292
|
+
*
|
|
293
|
+
* Crucially it does NOT fire on a real credential, because a password does not
|
|
294
|
+
* spell out its own variable name: `PASSWORD = "secret-test-password"` reduces to
|
|
295
|
+
* `""` vs `secrettestpassword` and still reports.
|
|
296
|
+
*/
|
|
297
|
+
function valueMirrorsItsOwnName(value, varName) {
|
|
298
|
+
const name = alphanumericLower(varName);
|
|
299
|
+
const val = alphanumericLower(value);
|
|
300
|
+
if (val.length < 4 || name.length < 4)
|
|
301
|
+
return false;
|
|
302
|
+
if (val === name)
|
|
303
|
+
return true;
|
|
304
|
+
// The name minus its credential nouns: PATTERNS_KEY -> patterns.
|
|
305
|
+
const stripped = [...CREDENTIAL_NOUNS, 'auth', 'api'].reduce((acc, noun) => acc.split(noun).join(''), name);
|
|
306
|
+
if (stripped.length >= 4 && stripped === val)
|
|
307
|
+
return true;
|
|
308
|
+
// The value is the name minus a trailing descriptor:
|
|
309
|
+
// SERVICE_ACCOUNT_TOKEN_DOC_TYPE -> service_account_token.
|
|
310
|
+
return val.length >= 6 && name.startsWith(val);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* True when the value is provably not a credential, whatever its variable is
|
|
314
|
+
* called. Applies to EVERY language's Layer 1, not just the newest ones.
|
|
315
|
+
*
|
|
316
|
+
* Found 2026-08-17 by scanning the real Elasticsearch repository: 59 findings in
|
|
317
|
+
* production source, almost all of them constants whose name carries a credential
|
|
318
|
+
* word and whose value is a settings key, a path, an algorithm, or a log message.
|
|
319
|
+
* At 10 points each that buries the real findings, which is worse than missing
|
|
320
|
+
* them - a rule nobody trusts gets switched off.
|
|
321
|
+
*
|
|
322
|
+
* THE GATE COMES FIRST and it is what makes the rest safe: anything containing a
|
|
323
|
+
* long unbroken alphanumeric run is treated as a credential and never exempted, so
|
|
324
|
+
* a base64 blob or a 64-character hex token cannot be talked away by the shapes
|
|
325
|
+
* below. Deliberately NOT included is any "looks random enough" requirement -
|
|
326
|
+
* that would drop weak-but-real passwords like `secret-test-password`, trading a
|
|
327
|
+
* visible false positive for an invisible false negative on the highest-value rule
|
|
328
|
+
* in the product. Noise is annoying; a missed credential is the actual danger.
|
|
329
|
+
*/
|
|
330
|
+
function valueIsNotACredential(value, varName) {
|
|
331
|
+
const v = value.trim();
|
|
332
|
+
// Checked BEFORE the gate, because both are unambiguous whatever they contain: a
|
|
333
|
+
// credential is never a URL, and never a command-line flag. `--validElastic
|
|
334
|
+
// LicenseKeyConfirmed=` is a 31-character run and would otherwise be held back
|
|
335
|
+
// by the gate below.
|
|
336
|
+
if (v.includes('://') || v.startsWith('--'))
|
|
337
|
+
return true;
|
|
338
|
+
// Anything credential-shaped is never exempted, whatever else it resembles. This
|
|
339
|
+
// is what makes every rule below safe to apply.
|
|
340
|
+
if (looksCredentialShaped(v))
|
|
341
|
+
return false;
|
|
342
|
+
if (valueMirrorsItsOwnName(v, varName))
|
|
343
|
+
return true;
|
|
344
|
+
// A LOG MESSAGE, not prose in general - and the distinction is the whole point.
|
|
345
|
+
//
|
|
346
|
+
// The first version of this read "four or more words is a sentence, so it is not
|
|
347
|
+
// a password", and the accuracy benchmark rejected it within a minute:
|
|
348
|
+
// `@password = "correct horse battery staple"` is four words and is a real
|
|
349
|
+
// passphrase, the XKCD form that security guidance actively recommends. Missing
|
|
350
|
+
// it would have been the exact trade this function's header refuses to make.
|
|
351
|
+
//
|
|
352
|
+
// So a log message has to prove itself: five or more words AND a mark that prose
|
|
353
|
+
// carries and a passphrase does not - a capitalised opening, punctuation, or a
|
|
354
|
+
// digit. "Internal cloud API key minted for cross-project datafeed" qualifies;
|
|
355
|
+
// "correct horse battery staple" does not, and neither does its capitalised form,
|
|
356
|
+
// because four words is under the floor either way.
|
|
357
|
+
const words = v.split(/\s+/).filter(Boolean);
|
|
358
|
+
if (words.length >= 5 && (/^[A-Z]/.test(v) || /[.,;:!?()[\]{}]/.test(v) || /\d/.test(v)))
|
|
359
|
+
return true;
|
|
360
|
+
// A path, with or without a leading slash: data/rows.ndjson,
|
|
361
|
+
// /api/v1/monitor/dep-graph, computeMetadata/v1/instance/service-accounts
|
|
362
|
+
if (/^\/?[\w.-]+(?:\/[\w.-]+)+\/?$/.test(v))
|
|
363
|
+
return true;
|
|
364
|
+
// A namespaced setting key: index.store.snapshot.partial
|
|
365
|
+
if ((v.match(/\./g) ?? []).length >= 2 && /^[\w.-]+$/.test(v))
|
|
366
|
+
return true;
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Layer 1: Java fields, locals, and setter calls.
|
|
371
|
+
*
|
|
372
|
+
* Java has NO `const`/`let`/`var`-style opener on a field, which is why routing
|
|
373
|
+
* it to the TypeScript detector left it detecting nothing at all. The everyday
|
|
374
|
+
* shape - `private static final String apiKey = "..."` - matched no pattern in
|
|
375
|
+
* this file, so the only Java secrets ever reported were the ones Layer 2 caught
|
|
376
|
+
* by token FORMAT. A committed database password has no recognisable format, so
|
|
377
|
+
* it was invisible.
|
|
378
|
+
*
|
|
379
|
+
* `var` is included because Java 10+ allows it for locals, and setter calls are
|
|
380
|
+
* included because `.setPassword("literal")` is as common in Java config code as
|
|
381
|
+
* a field assignment and is the same mistake.
|
|
382
|
+
*/
|
|
383
|
+
function detectLayer1Java(source) {
|
|
384
|
+
const results = [];
|
|
385
|
+
const push = (index, varName, value) => {
|
|
386
|
+
if (!varName || !value || value.length < 8)
|
|
387
|
+
return;
|
|
388
|
+
if (!hasSuspectKeyword(varName) || isPlaceholder(value, varName))
|
|
389
|
+
return;
|
|
390
|
+
if (namesAFieldRatherThanHoldingOne(value))
|
|
391
|
+
return;
|
|
392
|
+
if (valueIsNotACredential(value, varName))
|
|
393
|
+
return;
|
|
394
|
+
results.push({
|
|
395
|
+
line: lineOf(source, index),
|
|
396
|
+
variable: varName,
|
|
397
|
+
pattern: 'suspicious-name',
|
|
398
|
+
message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`,
|
|
399
|
+
value,
|
|
400
|
+
maskedValue: maskSecret(value),
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
// Field or local declaration. The modifier list is optional and unordered, so
|
|
404
|
+
// it is matched as a repeated group rather than a fixed sequence.
|
|
405
|
+
const declRe = /(?:(?:public|private|protected|static|final|transient|volatile|synchronized)\s+)*\b(?:String|CharSequence|var)\s+([A-Za-z_$][\w$]*)\s*=\s*"([^"\n]{4,500})"/g;
|
|
406
|
+
let m;
|
|
407
|
+
while ((m = declRe.exec(source)) !== null)
|
|
408
|
+
push(m.index, m[1], m[2]);
|
|
409
|
+
// `.setPassword("...")` / `.setApiKey("...")`: the method name carries the
|
|
410
|
+
// suspect word, so it plays the part the variable name plays above.
|
|
411
|
+
const setterRe = /\.\s*(set[A-Z][\w$]*)\s*\(\s*"([^"\n]{4,500})"\s*\)/g;
|
|
412
|
+
while ((m = setterRe.exec(source)) !== null)
|
|
413
|
+
push(m.index, m[1].slice(3), m[2]);
|
|
414
|
+
// `props.put("spring.datasource.password", "...")` and `setProperty(...)`:
|
|
415
|
+
// here the KEY literal is the name, not the method.
|
|
416
|
+
const keyedRe = /\b(?:put|setProperty|addProperty|set)\s*\(\s*"([\w.\-]+)"\s*,\s*"([^"\n]{4,500})"\s*\)/g;
|
|
417
|
+
while ((m = keyedRe.exec(source)) !== null)
|
|
418
|
+
push(m.index, m[1], m[2]);
|
|
419
|
+
return results;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Nouns that NAME a secret, as opposed to describing a mechanism.
|
|
423
|
+
*
|
|
424
|
+
* A `.properties` file is mostly settings, and a settings key mentioning
|
|
425
|
+
* authentication somewhere in its path is not announcing a credential. The JDK's
|
|
426
|
+
* own `conf/net.properties` ships `jdk.http.ntlm.transparentAuth=disabled`,
|
|
427
|
+
* which the general suspect-keyword test reports: the key contains `auth` and
|
|
428
|
+
* the value is eight characters long. `auth` is the weakest word in the set for
|
|
429
|
+
* exactly this reason - it qualifies a mechanism - so a properties key must end
|
|
430
|
+
* in a word that names the secret itself.
|
|
431
|
+
*
|
|
432
|
+
* `spring.datasource.password` ends in `password` and still fires;
|
|
433
|
+
* `jdk.http.auth.tunneling.disabledSchemes` ends in `disabledSchemes` and does
|
|
434
|
+
* not. Found 2026-08-16 by scanning a real JDK installation, not a fixture.
|
|
435
|
+
*/
|
|
436
|
+
const CREDENTIAL_NOUNS = new Set([
|
|
437
|
+
'key', 'token', 'secret', 'password', 'passwd', 'pwd', 'credential',
|
|
438
|
+
'credentials', 'apikey', 'accesskey', 'privatekey',
|
|
439
|
+
]);
|
|
440
|
+
/** The final dotted-or-camelCase segment of a properties key. */
|
|
441
|
+
function finalKeySegment(key) {
|
|
442
|
+
const segments = key
|
|
443
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
444
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
445
|
+
.toLowerCase()
|
|
446
|
+
.split(/\s+/)
|
|
447
|
+
.filter(Boolean);
|
|
448
|
+
return segments[segments.length - 1] ?? '';
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Layer 1: `.properties` config files.
|
|
452
|
+
*
|
|
453
|
+
* These are the reason `.properties` is a registered Java extension at all: a
|
|
454
|
+
* `spring.datasource.password` is the most commonly committed Java credential.
|
|
455
|
+
* They are key/value pairs with no quotes and no declaration keyword, so nothing
|
|
456
|
+
* else in this file could ever have matched one.
|
|
457
|
+
*/
|
|
458
|
+
function detectLayer1Properties(source) {
|
|
459
|
+
const results = [];
|
|
460
|
+
const lines = source.split('\n');
|
|
461
|
+
lines.forEach((rawLine, index) => {
|
|
462
|
+
const line = rawLine.trim();
|
|
463
|
+
// `#` and `!` both start a comment in the .properties format.
|
|
464
|
+
if (line === '' || line.startsWith('#') || line.startsWith('!'))
|
|
465
|
+
return;
|
|
466
|
+
const match = /^([\w.\-/]+)[ \t]*[=:][ \t]*(.*)$/.exec(line);
|
|
467
|
+
if (!match)
|
|
468
|
+
return;
|
|
469
|
+
const key = match[1];
|
|
470
|
+
const value = match[2].trim();
|
|
471
|
+
// The key must END in a word that names a secret, not merely contain one
|
|
472
|
+
// somewhere - see CREDENTIAL_NOUNS.
|
|
473
|
+
if (!CREDENTIAL_NOUNS.has(finalKeySegment(key)))
|
|
474
|
+
return;
|
|
475
|
+
if (value.length < 8 || isPlaceholder(value, key))
|
|
476
|
+
return;
|
|
477
|
+
if (namesAFieldRatherThanHoldingOne(value))
|
|
478
|
+
return;
|
|
479
|
+
if (valueIsNotACredential(value, key))
|
|
480
|
+
return;
|
|
481
|
+
// A value that is only a `${...}` placeholder is resolved at runtime from
|
|
482
|
+
// the environment or a secret store. That is the FIX for this rule, so
|
|
483
|
+
// flagging it would punish the correct pattern - the same exemption the
|
|
484
|
+
// .csproj reader gives `$(MSBuildProperty)` references.
|
|
485
|
+
if (/^\$\{[^}]*\}$/.test(value))
|
|
486
|
+
return;
|
|
487
|
+
results.push({
|
|
488
|
+
line: index + 1,
|
|
489
|
+
variable: key,
|
|
490
|
+
pattern: 'suspicious-name',
|
|
491
|
+
message: `Possible hardcoded secret: ${key} set directly in a properties file`,
|
|
492
|
+
value,
|
|
493
|
+
maskedValue: maskSecret(value),
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
return results;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Layer 1: Rust bindings, constants, statics, and struct-literal fields.
|
|
500
|
+
*
|
|
501
|
+
* `let` and `const` happened to work through the TypeScript detector, since it
|
|
502
|
+
* matches those keywords - but `static API_KEY: &str` did not, and neither did
|
|
503
|
+
* a struct literal, which is how configuration is usually written in Rust.
|
|
504
|
+
*/
|
|
505
|
+
function detectLayer1Rust(source) {
|
|
506
|
+
const results = [];
|
|
507
|
+
const push = (index, varName, value) => {
|
|
508
|
+
if (!varName || !value || value.length < 8)
|
|
509
|
+
return;
|
|
510
|
+
if (!hasSuspectKeyword(varName) || isPlaceholder(value, varName))
|
|
511
|
+
return;
|
|
512
|
+
if (namesAFieldRatherThanHoldingOne(value))
|
|
513
|
+
return;
|
|
514
|
+
if (valueIsNotACredential(value, varName))
|
|
515
|
+
return;
|
|
516
|
+
results.push({
|
|
517
|
+
line: lineOf(source, index),
|
|
518
|
+
variable: varName,
|
|
519
|
+
pattern: 'suspicious-name',
|
|
520
|
+
message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`,
|
|
521
|
+
value,
|
|
522
|
+
maskedValue: maskSecret(value),
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
// The type annotation is optional on `let` and required on `const`/`static`,
|
|
526
|
+
// so it is matched loosely up to the `=`.
|
|
527
|
+
const bindingRe = /\b(?:let(?:\s+mut)?|const|static(?:\s+mut)?)\s+([A-Za-z_]\w*)\s*(?::\s*[^=\n]+)?=\s*"([^"\n]{4,500})"/g;
|
|
528
|
+
let m;
|
|
529
|
+
while ((m = bindingRe.exec(source)) !== null)
|
|
530
|
+
push(m.index, m[1], m[2]);
|
|
531
|
+
// Struct literal or map entry: `Config { api_key: "..." }`. Gated on the
|
|
532
|
+
// suspect-keyword check like everything else, which is what keeps a match arm
|
|
533
|
+
// or a type position from producing noise.
|
|
534
|
+
const fieldRe = /(?:^|[{,])\s*([a-z_]\w*)\s*:\s*"([^"\n]{4,500})"/gm;
|
|
535
|
+
while ((m = fieldRe.exec(source)) !== null)
|
|
536
|
+
push(m.index, m[1], m[2]);
|
|
537
|
+
return results;
|
|
538
|
+
}
|
|
189
539
|
// Layer 2: Known secret format patterns — language-agnostic, scan full source
|
|
190
540
|
const KNOWN_PATTERNS = [
|
|
191
541
|
{ re: /sk-[a-zA-Z0-9]{10,}/g, name: 'OpenAI/Stripe secret key (sk-)' },
|
|
@@ -229,13 +579,19 @@ function detectHardcodedSecrets(source, config, language, filePath = '') {
|
|
|
229
579
|
// and not real. Skip the suspicious-name heuristic (Layer 1) there, but keep
|
|
230
580
|
// Layer 2 so a genuinely leaked provider token (AWS/Stripe/GitHub/...) is still
|
|
231
581
|
// flagged even when it appears in a test.
|
|
582
|
+
// A `.properties` file is key/value config, not Java source, so it gets its
|
|
583
|
+
// own reader rather than the Java one. It is matched on the PATH because the
|
|
584
|
+
// registry maps the extension to the `java` language key.
|
|
585
|
+
const isPropertiesFile = /\.properties$/i.test(filePath);
|
|
232
586
|
const layer1 = (0, guards_1.isTestFile)(filePath)
|
|
233
587
|
? []
|
|
234
588
|
: language === 'go' ? detectLayer1Go(source) :
|
|
235
589
|
language === 'python' ? detectLayer1Py(source) :
|
|
236
590
|
language === 'ruby' ? detectLayer1Ruby(source) :
|
|
237
591
|
language === 'csharp' ? detectLayer1CSharp(source) :
|
|
238
|
-
|
|
592
|
+
language === 'java' ? (isPropertiesFile ? detectLayer1Properties(source) : detectLayer1Java(source)) :
|
|
593
|
+
language === 'rust' ? detectLayer1Rust(source) :
|
|
594
|
+
detectLayer1Ts(source);
|
|
239
595
|
const layer2 = detectLayer2(source);
|
|
240
596
|
// Suppress Layer 2 hits on lines already flagged by Layer 1 (avoid duplicate Problems entries)
|
|
241
597
|
const layer1Lines = new Set(layer1.map(s => s.line));
|