circle-ir 3.126.0 → 3.129.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/analysis/passes/language-sources-pass.d.ts +72 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +674 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +405 -0
- package/package.json +1 -1
|
@@ -206,6 +206,11 @@ export class LanguageSourcesPass {
|
|
|
206
206
|
if (language === 'go') {
|
|
207
207
|
additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
|
|
208
208
|
additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
|
|
209
|
+
// Sprint 78 (#190): Go ECB-mode weak-crypto detection.
|
|
210
|
+
const goMisconfigFindings = findGoPatternFindings(code, graph.ir.meta.file);
|
|
211
|
+
for (const finding of goMisconfigFindings) {
|
|
212
|
+
ctx.addFinding(finding);
|
|
213
|
+
}
|
|
209
214
|
}
|
|
210
215
|
// -- Python: safe-handler sanitizer detectors (cognium-dev #114 Sprint 31) --
|
|
211
216
|
if (language === 'python') {
|
|
@@ -217,6 +222,9 @@ export class LanguageSourcesPass {
|
|
|
217
222
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
218
223
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
219
224
|
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
225
|
+
// Sprint 77a (#216 Pattern X): Jinja2 Environment(autoescape=...) +
|
|
226
|
+
// .render(...) sanitizer.
|
|
227
|
+
additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
|
|
220
228
|
// Sprint 71 (#190): pattern-based misconfig findings for subscript/context
|
|
221
229
|
// assignment shapes (cors-wildcard-origin, xfo-csp-mismatch, tls-verify-
|
|
222
230
|
// disabled) that the language-agnostic detectors miss in Python.
|
|
@@ -229,12 +237,30 @@ export class LanguageSourcesPass {
|
|
|
229
237
|
if (language === 'rust') {
|
|
230
238
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
231
239
|
additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
|
|
240
|
+
// Sprint 77a (#216 Pattern X): argv-form Command::new(literal).arg(...)
|
|
241
|
+
// sanitizer.
|
|
242
|
+
additionalSanitizers.push(...findRustArgvCommandSanitizers(code));
|
|
232
243
|
// Sprint 71 (#190): Rust reqwest builder `danger_accept_invalid_*(true)`
|
|
233
244
|
// is `tls-verify-disabled` — same rule as the Python/JS shapes.
|
|
234
245
|
const rustMisconfigFindings = findRustPatternFindings(code, graph.ir.meta.file);
|
|
235
246
|
for (const finding of rustMisconfigFindings) {
|
|
236
247
|
ctx.addFinding(finding);
|
|
237
248
|
}
|
|
249
|
+
// Sprint 78 (#190): additional Rust misconfig pattern detectors —
|
|
250
|
+
// hardcoded-credential, insecure-cookie (builder chain),
|
|
251
|
+
// jwt-verify-disabled, weak-crypto (raw ECB block ops).
|
|
252
|
+
for (const finding of findRustHardcodedCredentialFindings(code, graph.ir.meta.file)) {
|
|
253
|
+
ctx.addFinding(finding);
|
|
254
|
+
}
|
|
255
|
+
for (const finding of findRustInsecureCookieFindings(code, graph.ir.meta.file)) {
|
|
256
|
+
ctx.addFinding(finding);
|
|
257
|
+
}
|
|
258
|
+
for (const finding of findRustJwtVerifyDisabledFindings(code, graph.ir.meta.file)) {
|
|
259
|
+
ctx.addFinding(finding);
|
|
260
|
+
}
|
|
261
|
+
for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
|
|
262
|
+
ctx.addFinding(finding);
|
|
263
|
+
}
|
|
238
264
|
}
|
|
239
265
|
// -- JavaScript/TypeScript: Sprint 73 (#216 Pattern A + B) — ETE
|
|
240
266
|
// sanitizer-chain recognition (JSON.parse / bcrypt.hash / csv
|
|
@@ -246,12 +272,27 @@ export class LanguageSourcesPass {
|
|
|
246
272
|
additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
|
|
247
273
|
// Sprint 75 (#216 Pattern D): JS SSRF allow-list guard (var-aware).
|
|
248
274
|
additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
|
|
275
|
+
// Sprint 78 (#190): JS misconfig pattern findings — libxmljs noent:true.
|
|
276
|
+
for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
|
|
277
|
+
ctx.addFinding(finding);
|
|
278
|
+
}
|
|
249
279
|
}
|
|
250
280
|
// -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
|
|
251
281
|
// fromJson recognized as ETE terminator (does not affect
|
|
252
282
|
// configured `deserialization` sinks).
|
|
253
283
|
if (language === 'java') {
|
|
254
284
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
285
|
+
// Sprint 76 (#216 Pattern B): Java inline sanitizer recognition.
|
|
286
|
+
additionalSanitizers.push(...findJavaPathNormalizeStartsWithGuardSanitizers(code));
|
|
287
|
+
additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
|
|
288
|
+
// Sprint 77a (#216 Pattern X): argv-form exec sanitizer.
|
|
289
|
+
additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
|
|
290
|
+
// Sprint 78 (#190): Java misconfig pattern findings —
|
|
291
|
+
// jwt-verify-disabled (auth0 JWT.decode bare) +
|
|
292
|
+
// tls-verify-disabled (empty-body X509TrustManager).
|
|
293
|
+
for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
|
|
294
|
+
ctx.addFinding(finding);
|
|
295
|
+
}
|
|
255
296
|
}
|
|
256
297
|
// Sprint 70 (#151): cross-language env-secret → external-network exfiltration.
|
|
257
298
|
// Pattern findings only — no taint flow required (composed-flow shape that
|
|
@@ -2928,6 +2969,275 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
|
|
|
2928
2969
|
}
|
|
2929
2970
|
return sanitizers;
|
|
2930
2971
|
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Java: Path.resolve(...).normalize() + startsWith(ROOT) guard
|
|
2974
|
+
* sanitizer (cognium-dev #216 Sprint 76 Pattern B).
|
|
2975
|
+
*
|
|
2976
|
+
* Pattern recognized:
|
|
2977
|
+
*
|
|
2978
|
+
* private static final Path ROOT = Paths.get("/data");
|
|
2979
|
+
* public Path safe(String name) throws Exception {
|
|
2980
|
+
* Path full = ROOT.resolve(name).normalize();
|
|
2981
|
+
* if (!full.startsWith(ROOT)) throw new SecurityException("escape");
|
|
2982
|
+
* return full;
|
|
2983
|
+
* }
|
|
2984
|
+
*
|
|
2985
|
+
* Note: `.normalize()` alone is not safe (absolute-path arguments
|
|
2986
|
+
* replace ROOT entirely); the load-bearing check is the subsequent
|
|
2987
|
+
* `<full>.startsWith(<ROOT>)` guard with a terminator. Both the
|
|
2988
|
+
* normalize chain and the matching guard must be present to emit
|
|
2989
|
+
* the sanitizer.
|
|
2990
|
+
*
|
|
2991
|
+
* Emits a `path_traversal` + `external_taint_escape` sanitizer at
|
|
2992
|
+
* the resolve line and at every subsequent line that references the
|
|
2993
|
+
* normalized variable.
|
|
2994
|
+
*/
|
|
2995
|
+
function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
2996
|
+
const sanitizers = [];
|
|
2997
|
+
const lines = code.split('\n');
|
|
2998
|
+
// <var> = <root>.resolve(<arg>).normalize()
|
|
2999
|
+
// Also accepts Paths.get(<root>, <arg>).normalize() and
|
|
3000
|
+
// Path.of(<root>, <arg>).normalize() chained forms.
|
|
3001
|
+
const resolveNormalizeRe = /\b([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*\.\s*resolve\s*\([^)]*\)\s*\.\s*normalize\s*\(\s*\)/;
|
|
3002
|
+
const startsWithGuardRe = (varName, rootName) => new RegExp(`if\\s*\\(\\s*!\\s*${varName}\\s*\\.\\s*startsWith\\s*\\(\\s*${rootName}\\s*\\)\\s*\\)`);
|
|
3003
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
3004
|
+
// First pass: find every normalize-chain declaration and remember
|
|
3005
|
+
// its line + variable + root identifier.
|
|
3006
|
+
const candidates = [];
|
|
3007
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3008
|
+
const m = resolveNormalizeRe.exec(lines[i]);
|
|
3009
|
+
if (!m)
|
|
3010
|
+
continue;
|
|
3011
|
+
candidates.push({ line: i + 1, fullVar: m[1], rootVar: m[2] });
|
|
3012
|
+
}
|
|
3013
|
+
if (candidates.length === 0)
|
|
3014
|
+
return sanitizers;
|
|
3015
|
+
// Second pass: confirm each candidate has a matching startsWith
|
|
3016
|
+
// guard with a terminator on the same or next line.
|
|
3017
|
+
for (const c of candidates) {
|
|
3018
|
+
const guardRe = startsWithGuardRe(c.fullVar, c.rootVar);
|
|
3019
|
+
let guardLine = -1;
|
|
3020
|
+
// Search ahead up to 6 lines for the guard.
|
|
3021
|
+
for (let l = c.line; l < Math.min(lines.length, c.line + 6); l++) {
|
|
3022
|
+
if (!guardRe.test(lines[l]))
|
|
3023
|
+
continue;
|
|
3024
|
+
// The terminator may be on the same line (single-line if) or on
|
|
3025
|
+
// the next line (block-form if).
|
|
3026
|
+
if (terminatorRe.test(lines[l]) ||
|
|
3027
|
+
(l + 1 < lines.length && terminatorRe.test(lines[l + 1]))) {
|
|
3028
|
+
guardLine = l + 1;
|
|
3029
|
+
break;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
if (guardLine < 0)
|
|
3033
|
+
continue;
|
|
3034
|
+
// Emit on the resolve line and on every subsequent line that
|
|
3035
|
+
// references the normalized variable (covers the `return full;`
|
|
3036
|
+
// / `Files.read(full)` / etc. sink sites).
|
|
3037
|
+
const varRefRe = new RegExp(`\\b${c.fullVar}\\b`);
|
|
3038
|
+
sanitizers.push({
|
|
3039
|
+
type: 'java_path_normalize_startswith_guard',
|
|
3040
|
+
method: 'normalize',
|
|
3041
|
+
line: c.line,
|
|
3042
|
+
sanitizes: ['path_traversal', 'external_taint_escape'],
|
|
3043
|
+
});
|
|
3044
|
+
for (let l = c.line; l < lines.length; l++) {
|
|
3045
|
+
if (!varRefRe.test(lines[l]))
|
|
3046
|
+
continue;
|
|
3047
|
+
sanitizers.push({
|
|
3048
|
+
type: 'java_path_normalize_startswith_guard',
|
|
3049
|
+
method: 'normalize',
|
|
3050
|
+
line: l + 1,
|
|
3051
|
+
sanitizes: ['path_traversal', 'external_taint_escape'],
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return sanitizers;
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Java: inline CRLF/tab-strip wrapper at log-call site (cognium-dev
|
|
3059
|
+
* #216 Sprint 76 Pattern B).
|
|
3060
|
+
*
|
|
3061
|
+
* Pattern recognized:
|
|
3062
|
+
*
|
|
3063
|
+
* log.info("event=user_lookup value={}", user.replaceAll("[\\r\\n\\t]", "_"));
|
|
3064
|
+
*
|
|
3065
|
+
* The CRLF-strip `.replaceAll("[\\r\\n...]", ...)` (or
|
|
3066
|
+
* `.replace('\\n'|'\\r'|'\\t', ...)`) must appear as a *direct*
|
|
3067
|
+
* argument inside a recognized slf4j/log4j/JUL log-method call on
|
|
3068
|
+
* the same source line. A `.replaceAll(...)` on a different earlier
|
|
3069
|
+
* line (assigned to a temp variable) is NOT recognized — this
|
|
3070
|
+
* preserves TP firing on a separately-tainted log argument.
|
|
3071
|
+
*
|
|
3072
|
+
* Emits a `log_injection` + `external_taint_escape` sanitizer at
|
|
3073
|
+
* that line.
|
|
3074
|
+
*/
|
|
3075
|
+
function findJavaInlineCrlfStripLogSanitizers(code) {
|
|
3076
|
+
const sanitizers = [];
|
|
3077
|
+
const lines = code.split('\n');
|
|
3078
|
+
// Recognized log method receivers: common slf4j / log4j / JUL
|
|
3079
|
+
// identifiers. Restricted set avoids matching arbitrary
|
|
3080
|
+
// `something.info(...)` calls.
|
|
3081
|
+
const logCallStart = /\b(?:log|logger|LOG|LOGGER|slog|LOGGER_)\s*\.\s*(?:info|warn|error|debug|trace|fatal|severe|warning|fine|finer|finest|config)\s*\(/;
|
|
3082
|
+
// Threat-char regex literal classes that count as a CRLF strip.
|
|
3083
|
+
// Java string-literal escapes use \\r / \\n / \\t inside a "..."
|
|
3084
|
+
// source. We accept either character-class `[...\r\n...]` or a
|
|
3085
|
+
// single-char `replace('\n', ...)` / `replace('\r', ...)` /
|
|
3086
|
+
// `replace('\t', ...)` form.
|
|
3087
|
+
const crlfReplaceAll = /\.\s*replaceAll\s*\(\s*"\[[^"]*\\\\?[rnt][^"]*\]"/;
|
|
3088
|
+
const crlfReplaceChar = /\.\s*replace\s*\(\s*'(?:\\\\?[rnt])'\s*,/;
|
|
3089
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3090
|
+
const text = lines[i];
|
|
3091
|
+
if (!logCallStart.test(text))
|
|
3092
|
+
continue;
|
|
3093
|
+
if (!crlfReplaceAll.test(text) && !crlfReplaceChar.test(text))
|
|
3094
|
+
continue;
|
|
3095
|
+
sanitizers.push({
|
|
3096
|
+
type: 'java_inline_crlf_strip_log',
|
|
3097
|
+
method: 'replaceAll',
|
|
3098
|
+
line: i + 1,
|
|
3099
|
+
sanitizes: ['log_injection', 'external_taint_escape'],
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
return sanitizers;
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Java: argv-form `Runtime.getRuntime().exec(new String[]{...})` and
|
|
3106
|
+
* `new ProcessBuilder(new String[]{...})` sanitizer (cognium-dev #216
|
|
3107
|
+
* Sprint 77a Pattern X).
|
|
3108
|
+
*
|
|
3109
|
+
* Pattern recognized:
|
|
3110
|
+
*
|
|
3111
|
+
* Runtime.getRuntime().exec(new String[]{"echo", "--", arg});
|
|
3112
|
+
* new ProcessBuilder(new String[]{"ls", "-l", dir});
|
|
3113
|
+
*
|
|
3114
|
+
* Argv-form exec splits the program and arguments into a fixed array
|
|
3115
|
+
* with no shell interpretation, so a tainted argv element cannot
|
|
3116
|
+
* smuggle shell metacharacters into a separate command. The
|
|
3117
|
+
* single-string form `exec("echo " + arg)` is NOT matched and remains
|
|
3118
|
+
* a `command_injection` finding (TP-1 control).
|
|
3119
|
+
*
|
|
3120
|
+
* Emits a `command_injection` + `external_taint_escape` sanitizer at
|
|
3121
|
+
* that line.
|
|
3122
|
+
*/
|
|
3123
|
+
function findJavaArgvFormExecSanitizers(code) {
|
|
3124
|
+
const sanitizers = [];
|
|
3125
|
+
const lines = code.split('\n');
|
|
3126
|
+
// .exec(new String[]{...}) -- Runtime, Process, Desktop, etc.
|
|
3127
|
+
const argvExecRe = /\.\s*exec\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
3128
|
+
// new ProcessBuilder(new String[]{...})
|
|
3129
|
+
const argvPbRe = /\bnew\s+ProcessBuilder\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
3130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3131
|
+
const text = lines[i];
|
|
3132
|
+
if (!argvExecRe.test(text) && !argvPbRe.test(text))
|
|
3133
|
+
continue;
|
|
3134
|
+
sanitizers.push({
|
|
3135
|
+
type: 'java_argv_form_exec',
|
|
3136
|
+
method: 'exec',
|
|
3137
|
+
line: i + 1,
|
|
3138
|
+
sanitizes: ['command_injection', 'external_taint_escape'],
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
return sanitizers;
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Rust: argv-form `Command::new("literal").arg(...).arg(...)` sanitizer
|
|
3145
|
+
* (cognium-dev #216 Sprint 77a Pattern X).
|
|
3146
|
+
*
|
|
3147
|
+
* Pattern recognized:
|
|
3148
|
+
*
|
|
3149
|
+
* Command::new("grep").arg(p).arg("/var/log/app.log").status();
|
|
3150
|
+
*
|
|
3151
|
+
* Argv-form exec with a string-literal program splits arguments into
|
|
3152
|
+
* the argv slot with no shell interpretation. Tainted-program slot
|
|
3153
|
+
* `Command::new(prog).arg(...)` is NOT matched (TP-2 control), and
|
|
3154
|
+
* explicit shell-via-argv `Command::new("sh").arg("-c")` /
|
|
3155
|
+
* `Command::new("bash").arg("-c")` is excluded since `-c` re-enables
|
|
3156
|
+
* shell parsing of the tainted slot.
|
|
3157
|
+
*
|
|
3158
|
+
* Emits a `command_injection` + `external_taint_escape` sanitizer at
|
|
3159
|
+
* that line.
|
|
3160
|
+
*/
|
|
3161
|
+
function findRustArgvCommandSanitizers(code) {
|
|
3162
|
+
const sanitizers = [];
|
|
3163
|
+
const lines = code.split('\n');
|
|
3164
|
+
// Command::new("LITERAL").arg(... -- literal program + at least one .arg().
|
|
3165
|
+
const argvCommandRe = /\bCommand\s*::\s*new\s*\(\s*"[^"]*"\s*\)\s*\.\s*arg\s*\(/;
|
|
3166
|
+
// Shell-via-argv exclusion: Command::new("sh"/"bash"/...).arg("-c"/...)
|
|
3167
|
+
const shellArgvRe = /\bCommand\s*::\s*new\s*\(\s*"(?:sh|bash|zsh|ksh|dash|cmd(?:\.exe)?|powershell|pwsh)"\s*\)\s*\.\s*arg\s*\(\s*"-c"/;
|
|
3168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3169
|
+
const text = lines[i];
|
|
3170
|
+
if (!argvCommandRe.test(text))
|
|
3171
|
+
continue;
|
|
3172
|
+
if (shellArgvRe.test(text))
|
|
3173
|
+
continue;
|
|
3174
|
+
sanitizers.push({
|
|
3175
|
+
type: 'rust_argv_command',
|
|
3176
|
+
method: 'arg',
|
|
3177
|
+
line: i + 1,
|
|
3178
|
+
sanitizes: ['command_injection', 'external_taint_escape'],
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
return sanitizers;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Python: Jinja2 `Environment(..., autoescape=...)` + `.render(...)`
|
|
3185
|
+
* autoescape sanitizer (cognium-dev #216 Sprint 77a Pattern X).
|
|
3186
|
+
*
|
|
3187
|
+
* Pattern recognized:
|
|
3188
|
+
*
|
|
3189
|
+
* env = Environment(loader=..., autoescape=select_autoescape(["html"]))
|
|
3190
|
+
* env.get_template("hello.html").render(name=name)
|
|
3191
|
+
*
|
|
3192
|
+
* Autoescape-on environments html-escape all `.render(**ctx)` output
|
|
3193
|
+
* by default, blocking xss. `autoescape=False` / `None` / `0`
|
|
3194
|
+
* environments are NOT matched (TP-3 control), and only env vars
|
|
3195
|
+
* declared with an explicit `autoescape=` keyword argument are
|
|
3196
|
+
* recognized.
|
|
3197
|
+
*
|
|
3198
|
+
* Emits an `xss` + `external_taint_escape` sanitizer at every
|
|
3199
|
+
* `env.get_template(...).render(...)` chained call line.
|
|
3200
|
+
*/
|
|
3201
|
+
function findPythonJinjaAutoescapeSanitizers(code) {
|
|
3202
|
+
const sanitizers = [];
|
|
3203
|
+
const lines = code.split('\n');
|
|
3204
|
+
// Find Environment(...) assigned to an identifier and require an
|
|
3205
|
+
// autoescape= keyword somewhere on the same line. Two-regex form so
|
|
3206
|
+
// nested parens (e.g. PackageLoader("app", "templates")) inside the
|
|
3207
|
+
// Environment(...) call don't break the match.
|
|
3208
|
+
const envAssignRe = /\b([A-Za-z_]\w*)\s*=\s*Environment\s*\(/;
|
|
3209
|
+
const autoescapeOnRe = /\bautoescape\s*=/;
|
|
3210
|
+
// Explicit "off" forms that must NOT be recognized.
|
|
3211
|
+
const autoescapeOffRe = /\bautoescape\s*=\s*(?:False|None|0)\b/;
|
|
3212
|
+
const envVars = new Set();
|
|
3213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3214
|
+
const m = envAssignRe.exec(lines[i]);
|
|
3215
|
+
if (!m)
|
|
3216
|
+
continue;
|
|
3217
|
+
if (!autoescapeOnRe.test(lines[i]))
|
|
3218
|
+
continue;
|
|
3219
|
+
if (autoescapeOffRe.test(lines[i]))
|
|
3220
|
+
continue;
|
|
3221
|
+
envVars.add(m[1]);
|
|
3222
|
+
}
|
|
3223
|
+
if (envVars.size === 0)
|
|
3224
|
+
return sanitizers;
|
|
3225
|
+
// Emit at every `<env>.get_template(...).render(...)` chain.
|
|
3226
|
+
for (const envVar of envVars) {
|
|
3227
|
+
const renderChainRe = new RegExp(`\\b${envVar}\\s*\\.\\s*get_template\\s*\\([^)]*\\)\\s*\\.\\s*render\\s*\\(`);
|
|
3228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3229
|
+
if (!renderChainRe.test(lines[i]))
|
|
3230
|
+
continue;
|
|
3231
|
+
sanitizers.push({
|
|
3232
|
+
type: 'python_jinja_autoescape',
|
|
3233
|
+
method: 'render',
|
|
3234
|
+
line: i + 1,
|
|
3235
|
+
sanitizes: ['xss', 'external_taint_escape'],
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
return sanitizers;
|
|
3240
|
+
}
|
|
2931
3241
|
// ---------------------------------------------------------------------------
|
|
2932
3242
|
// Sprint 70 (#151) — external-secret-exfiltration composed-flow detection.
|
|
2933
3243
|
//
|
|
@@ -3430,4 +3740,368 @@ export function findRustPatternFindings(code, file) {
|
|
|
3430
3740
|
}
|
|
3431
3741
|
return out;
|
|
3432
3742
|
}
|
|
3743
|
+
// ---------------------------------------------------------------------------
|
|
3744
|
+
// Sprint 78 (#190) — Tier-2 misconfig pattern extensions for Rust, Java, Go,
|
|
3745
|
+
// and JS that the dedicated misconfig passes don't yet recognize.
|
|
3746
|
+
// ---------------------------------------------------------------------------
|
|
3747
|
+
/**
|
|
3748
|
+
* Rust `hardcoded-credential` (CWE-798). Pattern:
|
|
3749
|
+
* `(pub|const|static) <NAME>: &str = "literal";` where NAME matches
|
|
3750
|
+
* /api[_]?key|secret|token|password|passwd|pwd|auth/i and the literal is
|
|
3751
|
+
* non-trivial (length > 8 and not a placeholder).
|
|
3752
|
+
*/
|
|
3753
|
+
export function findRustHardcodedCredentialFindings(code, file) {
|
|
3754
|
+
const out = [];
|
|
3755
|
+
const lines = code.split('\n');
|
|
3756
|
+
const re = /\b(?:pub\s+)?(?:const|static)\s+([A-Z][A-Z0-9_]*)\s*:\s*&\s*'?[a-z_]*\s*str\s*=\s*"([^"]+)"/;
|
|
3757
|
+
const nameRe = /(?:^|_)(?:API[_]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|AUTH)(?:_|$)/i;
|
|
3758
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3759
|
+
const raw = lines[i];
|
|
3760
|
+
const trimmed = raw.trim();
|
|
3761
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
3762
|
+
continue;
|
|
3763
|
+
const m = trimmed.match(re);
|
|
3764
|
+
if (!m)
|
|
3765
|
+
continue;
|
|
3766
|
+
const name = m[1];
|
|
3767
|
+
const value = m[2];
|
|
3768
|
+
if (!nameRe.test(name))
|
|
3769
|
+
continue;
|
|
3770
|
+
if (value.length < 8)
|
|
3771
|
+
continue;
|
|
3772
|
+
if (/^(?:xxx|todo|fixme|placeholder|changeme)/i.test(value))
|
|
3773
|
+
continue;
|
|
3774
|
+
out.push({
|
|
3775
|
+
id: `hardcoded-credential-${file}-${i + 1}`,
|
|
3776
|
+
pass: 'language-sources',
|
|
3777
|
+
category: 'security',
|
|
3778
|
+
rule_id: 'hardcoded-credential',
|
|
3779
|
+
cwe: 'CWE-798',
|
|
3780
|
+
severity: 'high',
|
|
3781
|
+
level: 'error',
|
|
3782
|
+
message: `Hardcoded credential: const ${name} contains a literal secret value`,
|
|
3783
|
+
file,
|
|
3784
|
+
line: i + 1,
|
|
3785
|
+
snippet: trimmed.substring(0, 100),
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
return out;
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Rust `insecure-cookie` (CWE-1004 / CWE-614). Pattern:
|
|
3792
|
+
* `Cookie::build(...)` chain that explicitly calls `.secure(false)` or
|
|
3793
|
+
* `.http_only(false)` (or both). The dedicated `insecure-cookie-pass.ts`
|
|
3794
|
+
* handles the `format!("Set-Cookie: ...")` shape but not the actix-web
|
|
3795
|
+
* builder chain.
|
|
3796
|
+
*/
|
|
3797
|
+
export function findRustInsecureCookieFindings(code, file) {
|
|
3798
|
+
const out = [];
|
|
3799
|
+
const lines = code.split('\n');
|
|
3800
|
+
const builderRe = /\bCookie\s*::\s*build\s*\(/;
|
|
3801
|
+
const insecureFlagRe = /\.\s*(?:secure|http_only)\s*\(\s*false\s*\)/;
|
|
3802
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3803
|
+
const raw = lines[i];
|
|
3804
|
+
const trimmed = raw.trim();
|
|
3805
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
3806
|
+
continue;
|
|
3807
|
+
if (!builderRe.test(trimmed))
|
|
3808
|
+
continue;
|
|
3809
|
+
if (!insecureFlagRe.test(trimmed))
|
|
3810
|
+
continue;
|
|
3811
|
+
out.push({
|
|
3812
|
+
id: `insecure-cookie-${file}-${i + 1}`,
|
|
3813
|
+
pass: 'language-sources',
|
|
3814
|
+
category: 'security',
|
|
3815
|
+
rule_id: 'insecure-cookie',
|
|
3816
|
+
cwe: 'CWE-1004',
|
|
3817
|
+
severity: 'medium',
|
|
3818
|
+
level: 'warning',
|
|
3819
|
+
message: 'Insecure cookie: Cookie::build chain disables Secure / HttpOnly flag(s)',
|
|
3820
|
+
file,
|
|
3821
|
+
line: i + 1,
|
|
3822
|
+
snippet: trimmed.substring(0, 100),
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
return out;
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* Rust `jwt-verify-disabled` (CWE-347). Pattern:
|
|
3829
|
+
* `.insecure_disable_signature_validation()` method call on a
|
|
3830
|
+
* jsonwebtoken `Validation` value. Any presence of this method
|
|
3831
|
+
* disables the signature check.
|
|
3832
|
+
*/
|
|
3833
|
+
export function findRustJwtVerifyDisabledFindings(code, file) {
|
|
3834
|
+
const out = [];
|
|
3835
|
+
const lines = code.split('\n');
|
|
3836
|
+
const re = /\.\s*insecure_disable_signature_validation\s*\(/;
|
|
3837
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3838
|
+
const raw = lines[i];
|
|
3839
|
+
const trimmed = raw.trim();
|
|
3840
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
3841
|
+
continue;
|
|
3842
|
+
if (!re.test(trimmed))
|
|
3843
|
+
continue;
|
|
3844
|
+
out.push({
|
|
3845
|
+
id: `jwt-verify-disabled-${file}-${i + 1}`,
|
|
3846
|
+
pass: 'language-sources',
|
|
3847
|
+
category: 'security',
|
|
3848
|
+
rule_id: 'jwt-verify-disabled',
|
|
3849
|
+
cwe: 'CWE-347',
|
|
3850
|
+
severity: 'critical',
|
|
3851
|
+
level: 'error',
|
|
3852
|
+
message: 'JWT signature verification disabled: ' +
|
|
3853
|
+
'Validation::insecure_disable_signature_validation() forfeits signature enforcement',
|
|
3854
|
+
file,
|
|
3855
|
+
line: i + 1,
|
|
3856
|
+
snippet: trimmed.substring(0, 100),
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
return out;
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Rust `weak-crypto` (CWE-327). Pattern (raw ECB via `aes` crate):
|
|
3863
|
+
* any line that calls `.encrypt_block(` or `.decrypt_block(` on a
|
|
3864
|
+
* block-cipher receiver constructed via `Aes128::new` / `Aes192::new` /
|
|
3865
|
+
* `Aes256::new` / `Aes128Ecb` / `Aes256Ecb`. We collect the cipher
|
|
3866
|
+
* constructor lines in a first pass (variable name → seen) and emit
|
|
3867
|
+
* on every `.encrypt_block(` / `.decrypt_block(` line in the same file.
|
|
3868
|
+
*
|
|
3869
|
+
* The wrapped CBC/GCM/CTR forms go through `Cbc::<Aes128, ...>::new`
|
|
3870
|
+
* or `Aes128Gcm::new` — those do NOT call `.encrypt_block` directly
|
|
3871
|
+
* and so are not matched.
|
|
3872
|
+
*/
|
|
3873
|
+
export function findRustWeakCryptoEcbFindings(code, file) {
|
|
3874
|
+
const out = [];
|
|
3875
|
+
const lines = code.split('\n');
|
|
3876
|
+
const ctorRe = /\bAes(?:128|192|256)(?:Ecb)?\s*::\s*new\s*\(/;
|
|
3877
|
+
const blockOpRe = /\.\s*(encrypt_block|decrypt_block)\s*\(/;
|
|
3878
|
+
// First: confirm the file uses raw block-cipher construction (Aes*::new)
|
|
3879
|
+
// — without that, a stray `.encrypt_block(` on some other type isn't
|
|
3880
|
+
// necessarily ECB.
|
|
3881
|
+
let sawCtor = false;
|
|
3882
|
+
for (const line of lines) {
|
|
3883
|
+
if (ctorRe.test(line)) {
|
|
3884
|
+
sawCtor = true;
|
|
3885
|
+
break;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
if (!sawCtor)
|
|
3889
|
+
return out;
|
|
3890
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3891
|
+
const raw = lines[i];
|
|
3892
|
+
const trimmed = raw.trim();
|
|
3893
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
3894
|
+
continue;
|
|
3895
|
+
if (!blockOpRe.test(trimmed))
|
|
3896
|
+
continue;
|
|
3897
|
+
out.push({
|
|
3898
|
+
id: `weak-crypto-${file}-${i + 1}`,
|
|
3899
|
+
pass: 'language-sources',
|
|
3900
|
+
category: 'security',
|
|
3901
|
+
rule_id: 'weak-crypto',
|
|
3902
|
+
cwe: 'CWE-327',
|
|
3903
|
+
severity: 'high',
|
|
3904
|
+
level: 'error',
|
|
3905
|
+
message: 'Weak crypto (ECB mode): raw Aes::encrypt_block/decrypt_block ' +
|
|
3906
|
+
'leaks repeating-block patterns. Use AES-GCM, AES-CTR, or ' +
|
|
3907
|
+
'AES-CBC with an HMAC.',
|
|
3908
|
+
file,
|
|
3909
|
+
line: i + 1,
|
|
3910
|
+
snippet: trimmed.substring(0, 100),
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3913
|
+
return out;
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* Java pattern findings — Sprint 78 (#190):
|
|
3917
|
+
* - `jwt-verify-disabled` (CWE-347): bare `JWT.decode(<token>)` on the
|
|
3918
|
+
* auth0 `com.auth0.jwt.JWT` class. `decode` is documented as
|
|
3919
|
+
* "decode the token without performing any verification"; only
|
|
3920
|
+
* `JWT.require(...).build().verify(token)` enforces the signature.
|
|
3921
|
+
* - `tls-verify-disabled` (CWE-295): anonymous `X509TrustManager`
|
|
3922
|
+
* implementation whose `checkServerTrusted` body is empty
|
|
3923
|
+
* (returns void without raising). This trust-nothing implementation
|
|
3924
|
+
* accepts every certificate.
|
|
3925
|
+
*/
|
|
3926
|
+
export function findJavaPatternFindings(code, file) {
|
|
3927
|
+
const out = [];
|
|
3928
|
+
const lines = code.split('\n');
|
|
3929
|
+
// jwt-verify-disabled: `JWT.decode(<expr>)`. Anchored to the `JWT.`
|
|
3930
|
+
// receiver to avoid matching unrelated `decode(` calls on Base64,
|
|
3931
|
+
// URLDecoder, etc. Guarded against the safer `JWT.require(...).build()
|
|
3932
|
+
// .verify(...)` chain by requiring `decode` to appear without
|
|
3933
|
+
// `.verify(` later on the same line.
|
|
3934
|
+
const jwtDecodeRe = /\bJWT\s*\.\s*decode\s*\(/;
|
|
3935
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3936
|
+
const raw = lines[i];
|
|
3937
|
+
const trimmed = raw.trim();
|
|
3938
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
3939
|
+
continue;
|
|
3940
|
+
if (!jwtDecodeRe.test(trimmed))
|
|
3941
|
+
continue;
|
|
3942
|
+
if (/\.\s*verify\s*\(/.test(trimmed))
|
|
3943
|
+
continue;
|
|
3944
|
+
out.push({
|
|
3945
|
+
id: `jwt-verify-disabled-${file}-${i + 1}-decode`,
|
|
3946
|
+
pass: 'language-sources',
|
|
3947
|
+
category: 'security',
|
|
3948
|
+
rule_id: 'jwt-verify-disabled',
|
|
3949
|
+
cwe: 'CWE-347',
|
|
3950
|
+
severity: 'critical',
|
|
3951
|
+
level: 'error',
|
|
3952
|
+
message: 'JWT signature not verified: auth0 `JWT.decode(token)` parses ' +
|
|
3953
|
+
'without checking the signature. Use `JWT.require(<algorithm>)' +
|
|
3954
|
+
'.build().verify(token)` to enforce verification.',
|
|
3955
|
+
file,
|
|
3956
|
+
line: i + 1,
|
|
3957
|
+
snippet: trimmed.substring(0, 100),
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
// tls-verify-disabled: anonymous X509TrustManager with empty
|
|
3961
|
+
// checkServerTrusted body. Two-pass: locate the anonymous-class start
|
|
3962
|
+
// line (`new X509TrustManager() {`), then scan ahead for the
|
|
3963
|
+
// `checkServerTrusted(...)` method signature whose `{...}` body is
|
|
3964
|
+
// empty (no `throw`, no `if`).
|
|
3965
|
+
const anonStartRe = /\bnew\s+X509TrustManager\s*\(\s*\)\s*\{/;
|
|
3966
|
+
const checkServerSig = /\bcheckServerTrusted\s*\([^)]*\)\s*(?:throws\s+[^\{]*)?\{\s*\}/;
|
|
3967
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3968
|
+
const raw = lines[i];
|
|
3969
|
+
if (!anonStartRe.test(raw))
|
|
3970
|
+
continue;
|
|
3971
|
+
// Scan up to 15 lines ahead for the empty-body checkServerTrusted.
|
|
3972
|
+
const end = Math.min(lines.length, i + 16);
|
|
3973
|
+
let foundAt = -1;
|
|
3974
|
+
for (let j = i; j < end; j++) {
|
|
3975
|
+
if (checkServerSig.test(lines[j])) {
|
|
3976
|
+
foundAt = j;
|
|
3977
|
+
break;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
if (foundAt < 0)
|
|
3981
|
+
continue;
|
|
3982
|
+
out.push({
|
|
3983
|
+
id: `tls-verify-disabled-${file}-${foundAt + 1}`,
|
|
3984
|
+
pass: 'language-sources',
|
|
3985
|
+
category: 'security',
|
|
3986
|
+
rule_id: 'tls-verify-disabled',
|
|
3987
|
+
cwe: 'CWE-295',
|
|
3988
|
+
severity: 'high',
|
|
3989
|
+
level: 'error',
|
|
3990
|
+
message: 'TLS certificate verification disabled: anonymous X509TrustManager ' +
|
|
3991
|
+
'with empty checkServerTrusted body accepts every certificate.',
|
|
3992
|
+
file,
|
|
3993
|
+
line: foundAt + 1,
|
|
3994
|
+
snippet: lines[foundAt].trim().substring(0, 100),
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
return out;
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* Go pattern findings — Sprint 78 (#190):
|
|
4001
|
+
* - `weak-crypto` (CWE-327): raw ECB usage via `aes.NewCipher(...)`
|
|
4002
|
+
* followed by a direct `<cipher>.Encrypt(` / `.Decrypt(` call on the
|
|
4003
|
+
* constructed value (no `cipher.NewCBCEncrypter` / `NewGCM` / `NewCTR`
|
|
4004
|
+
* wrapper). The Go stdlib `aes.Cipher` exposes `Encrypt` / `Decrypt`
|
|
4005
|
+
* that operate on a single 16-byte block — calling these directly is
|
|
4006
|
+
* ECB mode.
|
|
4007
|
+
*
|
|
4008
|
+
* Algorithm:
|
|
4009
|
+
* 1. Collect every `<v>, _ := aes.NewCipher(...)` cipher variable.
|
|
4010
|
+
* 2. If the file contains a `cipher.NewGCM(<v>)` / `cipher.NewCBC*(<v>)`
|
|
4011
|
+
* / `cipher.NewCTR(<v>)` wrapping line for that variable, skip it
|
|
4012
|
+
* (wrapped mode is not ECB).
|
|
4013
|
+
* 3. Otherwise emit on every `<v>.Encrypt(` / `<v>.Decrypt(` line.
|
|
4014
|
+
*/
|
|
4015
|
+
export function findGoPatternFindings(code, file) {
|
|
4016
|
+
const out = [];
|
|
4017
|
+
const lines = code.split('\n');
|
|
4018
|
+
const cipherVars = new Set();
|
|
4019
|
+
const ctorRe = /\b([a-zA-Z_]\w*)\s*(?:,\s*[a-zA-Z_]\w*)?\s*:?=\s*aes\.NewCipher\s*\(/;
|
|
4020
|
+
for (const line of lines) {
|
|
4021
|
+
const m = line.match(ctorRe);
|
|
4022
|
+
if (m)
|
|
4023
|
+
cipherVars.add(m[1]);
|
|
4024
|
+
}
|
|
4025
|
+
if (cipherVars.size === 0)
|
|
4026
|
+
return out;
|
|
4027
|
+
// Drop wrapped ciphers (CBC/GCM/CTR/OFB/CFB) — those are not ECB.
|
|
4028
|
+
for (const v of Array.from(cipherVars)) {
|
|
4029
|
+
const wrapRe = new RegExp(`\\bcipher\\.New(?:GCM|CBCEncrypter|CBCDecrypter|CTR|OFB|CFBEncrypter|CFBDecrypter)\\s*\\(\\s*${v}\\b`);
|
|
4030
|
+
for (const line of lines) {
|
|
4031
|
+
if (wrapRe.test(line)) {
|
|
4032
|
+
cipherVars.delete(v);
|
|
4033
|
+
break;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
if (cipherVars.size === 0)
|
|
4038
|
+
return out;
|
|
4039
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4040
|
+
const raw = lines[i];
|
|
4041
|
+
const trimmed = raw.trim();
|
|
4042
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
4043
|
+
continue;
|
|
4044
|
+
for (const v of cipherVars) {
|
|
4045
|
+
const opRe = new RegExp(`\\b${v}\\s*\\.\\s*(?:Encrypt|Decrypt)\\s*\\(`);
|
|
4046
|
+
if (!opRe.test(trimmed))
|
|
4047
|
+
continue;
|
|
4048
|
+
out.push({
|
|
4049
|
+
id: `weak-crypto-${file}-${i + 1}`,
|
|
4050
|
+
pass: 'language-sources',
|
|
4051
|
+
category: 'security',
|
|
4052
|
+
rule_id: 'weak-crypto',
|
|
4053
|
+
cwe: 'CWE-327',
|
|
4054
|
+
severity: 'high',
|
|
4055
|
+
level: 'error',
|
|
4056
|
+
message: 'Weak crypto (ECB mode): raw aes.Cipher.Encrypt/Decrypt on a ' +
|
|
4057
|
+
'block leaks repeating-block patterns. Wrap with cipher.NewGCM, ' +
|
|
4058
|
+
'cipher.NewCTR, or cipher.NewCBCEncrypter + HMAC.',
|
|
4059
|
+
file,
|
|
4060
|
+
line: i + 1,
|
|
4061
|
+
snippet: trimmed.substring(0, 100),
|
|
4062
|
+
});
|
|
4063
|
+
break;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
return out;
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* JS pattern findings — Sprint 78 (#190):
|
|
4070
|
+
* - `xml-entity-expansion` (CWE-611 / CWE-776): libxmljs `parseXml`
|
|
4071
|
+
* (and `parseXmlString`) called with `{ noent: true }` resolves
|
|
4072
|
+
* external entities, enabling XXE / billion-laughs. The default is
|
|
4073
|
+
* `noent: false`; only the explicit-true form is unsafe.
|
|
4074
|
+
*/
|
|
4075
|
+
export function findJsPatternFindings(code, file) {
|
|
4076
|
+
const out = [];
|
|
4077
|
+
const lines = code.split('\n');
|
|
4078
|
+
const parseRe = /\blibxml(?:js)?\s*\.\s*parseXml(?:String)?\s*\(/;
|
|
4079
|
+
const noentTrueRe = /\bnoent\s*:\s*true\b/;
|
|
4080
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4081
|
+
const raw = lines[i];
|
|
4082
|
+
const trimmed = raw.trim();
|
|
4083
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
4084
|
+
continue;
|
|
4085
|
+
if (!parseRe.test(trimmed))
|
|
4086
|
+
continue;
|
|
4087
|
+
if (!noentTrueRe.test(trimmed))
|
|
4088
|
+
continue;
|
|
4089
|
+
out.push({
|
|
4090
|
+
id: `xml-entity-expansion-${file}-${i + 1}`,
|
|
4091
|
+
pass: 'language-sources',
|
|
4092
|
+
category: 'security',
|
|
4093
|
+
rule_id: 'xml-entity-expansion',
|
|
4094
|
+
cwe: 'CWE-611',
|
|
4095
|
+
severity: 'high',
|
|
4096
|
+
level: 'error',
|
|
4097
|
+
message: 'XML external entity resolution enabled: libxmljs parseXml ' +
|
|
4098
|
+
'called with `noent: true` resolves external entities (XXE / ' +
|
|
4099
|
+
'billion-laughs). Omit the flag or set `noent: false`.',
|
|
4100
|
+
file,
|
|
4101
|
+
line: i + 1,
|
|
4102
|
+
snippet: trimmed.substring(0, 100),
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
return out;
|
|
4106
|
+
}
|
|
3433
4107
|
//# sourceMappingURL=language-sources-pass.js.map
|