deep-slop 1.4.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.
@@ -0,0 +1,1198 @@
1
+ import { i as toLines, r as readFileContent } from "./file-utils-B_HFXhCs.js";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { execSync } from "node:child_process";
5
+
6
+ //#region src/security/audit.ts
7
+ function runWithTimeout(cmd, cwd, timeout) {
8
+ try {
9
+ return {
10
+ stdout: execSync(cmd, {
11
+ cwd,
12
+ timeout,
13
+ encoding: "utf-8",
14
+ stdio: [
15
+ "pipe",
16
+ "pipe",
17
+ "pipe"
18
+ ],
19
+ maxBuffer: 10 * 1024 * 1024
20
+ }),
21
+ stderr: "",
22
+ status: 0,
23
+ timedOut: false
24
+ };
25
+ } catch (err) {
26
+ const e = err;
27
+ return {
28
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
29
+ stderr: typeof e.stderr === "string" ? e.stderr : "",
30
+ status: e.status ?? null,
31
+ timedOut: e.killed === true || e.signal === "SIGTERM"
32
+ };
33
+ }
34
+ }
35
+ function makeAuditDiagnostic(rule, severity, message, help, opts) {
36
+ return {
37
+ filePath: "package.json",
38
+ engine: "security-deep",
39
+ rule,
40
+ severity,
41
+ message,
42
+ help,
43
+ line: 1,
44
+ column: 1,
45
+ category: "security",
46
+ fixable: opts?.fixable ?? false,
47
+ suggestion: opts?.suggestion,
48
+ detail: opts?.detail
49
+ };
50
+ }
51
+ function mapNpmSeverity(s) {
52
+ const lower = s.toLowerCase();
53
+ if (lower === "critical" || lower === "high") return "error";
54
+ if (lower === "moderate" || lower === "medium") return "warning";
55
+ return "info";
56
+ }
57
+ function npmAudit(rootDir, timeout) {
58
+ if (!existsSync(join(rootDir, "package-lock.json")) && !existsSync(join(rootDir, "package.json"))) return [];
59
+ const result = runWithTimeout("npm audit --json", rootDir, timeout);
60
+ if (result.timedOut) return [makeAuditDiagnostic("security-deep/dependency-vulnerability", "warning", "npm audit timed out", "Increase security.auditTimeout or check network connectivity.")];
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(result.stdout);
64
+ } catch {
65
+ return [makeAuditDiagnostic("security-deep/dependency-vulnerability", "warning", "npm audit produced non-JSON output", "Ensure npm is installed and the project has a valid package-lock.json.")];
66
+ }
67
+ const diagnostics = [];
68
+ if (parsed.advisories) for (const [, adv] of Object.entries(parsed.advisories)) diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", mapNpmSeverity(adv.severity), `Vulnerability in ${adv.module_name}: ${adv.title}`, `Update ${adv.module_name} to ${adv.patched_versions || "a patched version"}. ${adv.url}`, {
69
+ fixable: !!adv.patched_versions,
70
+ suggestion: {
71
+ type: "refactor",
72
+ text: `npm update ${adv.module_name}`,
73
+ confidence: .9,
74
+ reason: `Patched version available: ${adv.patched_versions || "unknown"}`
75
+ },
76
+ detail: {
77
+ module: adv.module_name,
78
+ vulnerableVersions: adv.vulnerable_versions,
79
+ patchedVersions: adv.patched_versions,
80
+ cwe: adv.cwe,
81
+ auditTool: "npm"
82
+ }
83
+ }));
84
+ if (parsed.vulnerabilities) for (const [, vuln] of Object.entries(parsed.vulnerabilities)) {
85
+ const isFixable = vuln.fixAvailable !== false && vuln.fixAvailable != null;
86
+ diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", mapNpmSeverity(vuln.severity), `Vulnerability in ${vuln.name} (${vuln.range})`, isFixable ? `Run 'npm audit fix' to apply available fixes for ${vuln.name}.` : `No automatic fix available for ${vuln.name}. Review and update manually.`, {
87
+ fixable: isFixable,
88
+ suggestion: {
89
+ type: "refactor",
90
+ text: isFixable ? "npm audit fix" : `npm update ${vuln.name}`,
91
+ confidence: isFixable ? .9 : .5,
92
+ reason: isFixable ? "npm audit fix can resolve this automatically" : "Manual review and update required"
93
+ },
94
+ detail: {
95
+ module: vuln.name,
96
+ range: vuln.range,
97
+ fixAvailable: vuln.fixAvailable,
98
+ auditTool: "npm"
99
+ }
100
+ }));
101
+ }
102
+ return diagnostics;
103
+ }
104
+ function pipAudit(rootDir, timeout) {
105
+ if (!existsSync(join(rootDir, "requirements.txt")) && !existsSync(join(rootDir, "Pipfile")) && !existsSync(join(rootDir, "pyproject.toml"))) return [];
106
+ const result = runWithTimeout("pip-audit --format=json", rootDir, timeout);
107
+ if (result.timedOut) return [makeAuditDiagnostic("security-deep/dependency-vulnerability", "warning", "pip-audit timed out", "Increase security.auditTimeout or check network connectivity.")];
108
+ if (result.status !== 0 && !result.stdout.trim()) return [];
109
+ let parsed;
110
+ try {
111
+ parsed = JSON.parse(result.stdout);
112
+ } catch {
113
+ return [];
114
+ }
115
+ const diagnostics = [];
116
+ if (parsed.dependencies) for (const dep of parsed.dependencies) for (const v of dep.vulns) {
117
+ const fixVersions = v.fix_versions?.join(", ") ?? v.vuln.fixed_versions?.join(", ");
118
+ const hasFix = (v.fix_versions?.length ?? 0) > 0 || (v.vuln.fixed_versions?.length ?? 0) > 0;
119
+ diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", "error", `Vulnerability in ${dep.package}@${dep.version}: ${v.vuln.id} - ${v.vuln.description}`, hasFix ? `Update ${dep.package} to ${fixVersions || "a patched version"}.` : `No patched version available for ${dep.package}. Review and mitigate manually.`, {
120
+ fixable: hasFix,
121
+ suggestion: {
122
+ type: "refactor",
123
+ text: `pip install --upgrade ${dep.package}`,
124
+ confidence: hasFix ? .85 : .3,
125
+ reason: hasFix ? `Fix available in version(s): ${fixVersions}` : "No known fix — review and apply mitigations"
126
+ },
127
+ detail: {
128
+ module: dep.package,
129
+ version: dep.version,
130
+ vulnId: v.vuln.id,
131
+ aliases: v.vuln.aliases,
132
+ fixVersions: v.fix_versions ?? v.vuln.fixed_versions,
133
+ auditTool: "pip-audit"
134
+ }
135
+ }));
136
+ }
137
+ if (parsed.vulnerabilities) for (const vuln of parsed.vulnerabilities) {
138
+ const hasFix = (vuln.fixed_versions?.length ?? 0) > 0;
139
+ const pkgName = vuln.name ?? "unknown";
140
+ const pkgVer = vuln.version ?? "unknown";
141
+ diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", "error", `Vulnerability in ${pkgName}@${pkgVer}: ${vuln.id ?? "unknown"} - ${vuln.description ?? "no description"}`, hasFix ? `Update ${pkgName} to ${vuln.fixed_versions?.join(", ") ?? "a patched version"}.` : `No patched version available for ${pkgName}. Review and mitigate manually.`, {
142
+ fixable: hasFix,
143
+ suggestion: {
144
+ type: "refactor",
145
+ text: `pip install --upgrade ${pkgName}`,
146
+ confidence: hasFix ? .85 : .3,
147
+ reason: hasFix ? `Fix available in version(s): ${vuln.fixed_versions?.join(", ")}` : "No known fix — review and apply mitigations"
148
+ },
149
+ detail: {
150
+ module: pkgName,
151
+ version: pkgVer,
152
+ vulnId: vuln.id,
153
+ fixVersions: vuln.fixed_versions,
154
+ auditTool: "pip-audit"
155
+ }
156
+ }));
157
+ }
158
+ return diagnostics;
159
+ }
160
+ function goVulnCheck(rootDir, timeout) {
161
+ if (!existsSync(join(rootDir, "go.mod"))) return [];
162
+ const result = runWithTimeout("govulncheck -json ./...", rootDir, timeout);
163
+ if (result.timedOut) return [makeAuditDiagnostic("security-deep/dependency-vulnerability", "warning", "govulncheck timed out", "Increase security.auditTimeout or check network connectivity.")];
164
+ if (!result.stdout.trim()) return [];
165
+ const diagnostics = [];
166
+ for (const line of result.stdout.split("\n")) {
167
+ if (!line.trim()) continue;
168
+ let entry;
169
+ try {
170
+ entry = JSON.parse(line);
171
+ } catch {
172
+ continue;
173
+ }
174
+ if (entry.finding) {
175
+ const finding = entry.finding;
176
+ const module = finding.trace?.[0]?.module ?? "unknown";
177
+ const version = finding.trace?.[0]?.version ?? "unknown";
178
+ const hasFix = !!finding.fixed;
179
+ diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", "error", `Vulnerability in ${module}@${version}: ${finding.osv ?? "unknown OSV"}`, hasFix ? `Update ${module} to ${finding.fixed}.` : `No patched version available for ${module}. Review and mitigate manually.`, {
180
+ fixable: hasFix,
181
+ suggestion: {
182
+ type: "refactor",
183
+ text: `go get ${module}@${finding.fixed ?? "latest"}`,
184
+ confidence: hasFix ? .85 : .3,
185
+ reason: hasFix ? `Fix available in version: ${finding.fixed}` : "No known fix — review and apply mitigations"
186
+ },
187
+ detail: {
188
+ module,
189
+ version,
190
+ osv: finding.osv,
191
+ fixed: finding.fixed,
192
+ auditTool: "govulncheck"
193
+ }
194
+ }));
195
+ }
196
+ }
197
+ return diagnostics;
198
+ }
199
+ function cargoAudit(rootDir, timeout) {
200
+ if (!existsSync(join(rootDir, "Cargo.lock"))) return [];
201
+ const result = runWithTimeout("cargo audit --json", rootDir, timeout);
202
+ if (result.timedOut) return [makeAuditDiagnostic("security-deep/dependency-vulnerability", "warning", "cargo audit timed out", "Increase security.auditTimeout or check network connectivity.")];
203
+ if (result.status !== 0 && !result.stdout.trim()) return [];
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(result.stdout);
207
+ } catch {
208
+ return [];
209
+ }
210
+ const diagnostics = [];
211
+ if (parsed.vulnerabilities?.list) for (const vuln of parsed.vulnerabilities.list) {
212
+ const hasFix = (vuln.versions.patched?.length ?? 0) > 0;
213
+ const mappedSeverity = (vuln.advisory.severity?.toLowerCase() ?? "high") === "low" ? "warning" : "error";
214
+ diagnostics.push(makeAuditDiagnostic("security-deep/dependency-vulnerability", mappedSeverity, `Vulnerability in ${vuln.package}: ${vuln.advisory.id} - ${vuln.advisory.title}`, hasFix ? `Update ${vuln.package} to ${vuln.versions.patched?.join(", ") ?? "a patched version"}. ${vuln.advisory.url}` : `No patched version available for ${vuln.package}. ${vuln.advisory.url}`, {
215
+ fixable: hasFix,
216
+ suggestion: {
217
+ type: "refactor",
218
+ text: `cargo update -p ${vuln.package}`,
219
+ confidence: hasFix ? .85 : .3,
220
+ reason: hasFix ? `Fix available in version(s): ${vuln.versions.patched?.join(", ")}` : "No known fix — review and apply mitigations"
221
+ },
222
+ detail: {
223
+ module: vuln.package,
224
+ advisoryId: vuln.advisory.id,
225
+ title: vuln.advisory.title,
226
+ patchedVersions: vuln.versions.patched,
227
+ url: vuln.advisory.url,
228
+ auditTool: "cargo-audit"
229
+ }
230
+ }));
231
+ }
232
+ return diagnostics;
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/utils/source-mask.ts
237
+ /** Pattern matchers for sensitive data */
238
+ const SENSITIVE_PATTERNS = [
239
+ {
240
+ pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/g,
241
+ replacement: "[REDACTED-KEY]",
242
+ label: "API key"
243
+ },
244
+ {
245
+ pattern: /\b(ghp_[a-zA-Z0-9]{36,})\b/g,
246
+ replacement: "[REDACTED-TOKEN]",
247
+ label: "GitHub PAT"
248
+ },
249
+ {
250
+ pattern: /\b(gho_[a-zA-Z0-9]{36,})\b/g,
251
+ replacement: "[REDACTED-TOKEN]",
252
+ label: "GitHub OAuth"
253
+ },
254
+ {
255
+ pattern: /\b(AKIA[A-Z0-9]{16})\b/g,
256
+ replacement: "[REDACTED-KEY]",
257
+ label: "AWS access key"
258
+ },
259
+ {
260
+ pattern: /\b(Bearer\s+)[a-zA-Z0-9\-_.]{20,}/gi,
261
+ replacement: "$1[REDACTED]",
262
+ label: "Bearer token"
263
+ },
264
+ {
265
+ pattern: /:\/\/([^:]+):([^@]+)@/g,
266
+ replacement: "://[REDACTED]:[REDACTED]@",
267
+ label: "URL credentials"
268
+ },
269
+ {
270
+ pattern: /\b(password|pwd|passwd|secret|token|apikey|api_key)\s*[:=]\s*["'][^"']{4,}["']/gi,
271
+ replacement: "$1=[REDACTED]",
272
+ label: "Password/secret assignment"
273
+ },
274
+ {
275
+ pattern: /\b(mongodb|postgres|mysql|redis):\/\/[^:]+:[^@]+@/gi,
276
+ replacement: "$1://[REDACTED]:[REDACTED]@",
277
+ label: "DB connection string"
278
+ },
279
+ {
280
+ pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
281
+ replacement: "[REDACTED-PRIVATE-KEY]",
282
+ label: "Private key"
283
+ }
284
+ ];
285
+ /** Redact sensitive data from a string */
286
+ function maskSecrets(text) {
287
+ let result = text;
288
+ for (const { pattern, replacement } of SENSITIVE_PATTERNS) result = result.replace(pattern, replacement);
289
+ return result;
290
+ }
291
+
292
+ //#endregion
293
+ //#region src/security/secrets.ts
294
+ function makeSecretDiagnostic(filePath, rule, severity, message, help, line, column, opts) {
295
+ return {
296
+ filePath,
297
+ engine: "security-deep",
298
+ rule,
299
+ severity,
300
+ message,
301
+ help,
302
+ line,
303
+ column,
304
+ category: "security",
305
+ fixable: opts?.fixable ?? false,
306
+ suggestion: opts?.suggestion,
307
+ detail: opts?.detail
308
+ };
309
+ }
310
+ function isTestFile$1(filePath) {
311
+ const normalized = filePath.replace(/\\/g, "/");
312
+ if (normalized.endsWith(".test.ts") || normalized.endsWith(".spec.ts")) return true;
313
+ if (normalized.endsWith(".test.js") || normalized.endsWith(".spec.js")) return true;
314
+ if (normalized.endsWith(".test.py") || normalized.endsWith(".spec.py")) return true;
315
+ if (normalized.endsWith(".test.go")) return true;
316
+ if (normalized.endsWith(".rs") && normalized.includes("/tests/")) return true;
317
+ if (/\/__tests__\//.test(normalized)) return true;
318
+ if (/(?:^|\/)(?:test|tests|spec|specs)\//.test(normalized)) return true;
319
+ return false;
320
+ }
321
+ const SECRET_PATTERNS = [
322
+ {
323
+ name: "API key",
324
+ pattern: /['"`](sk-[A-Za-z0-9_-]{10,})['"`]/g,
325
+ secretGroup: 1,
326
+ help: "Move API keys to environment variables or a secrets manager. Never commit secrets to source control.",
327
+ envVar: "SECRET_KEY",
328
+ secretType: "api-key"
329
+ },
330
+ {
331
+ name: "GitHub PAT",
332
+ pattern: /['"`](ghp_[A-Za-z0-9]{36,})['"`]/g,
333
+ secretGroup: 1,
334
+ help: "Move GitHub tokens to environment variables or a secrets manager.",
335
+ envVar: "GITHUB_TOKEN",
336
+ secretType: "api-key"
337
+ },
338
+ {
339
+ name: "GitHub OAuth token",
340
+ pattern: /['"`](gho_[A-Za-z0-9]{36,})['"`]/g,
341
+ secretGroup: 1,
342
+ help: "Move GitHub OAuth tokens to environment variables or a secrets manager.",
343
+ envVar: "GITHUB_OAUTH_TOKEN",
344
+ secretType: "token"
345
+ },
346
+ {
347
+ name: "GitHub fine-grained PAT",
348
+ pattern: /['"`](github_pat_[A-Za-z0-9_]{50,})['"`]/g,
349
+ secretGroup: 1,
350
+ help: "Move GitHub fine-grained PATs to environment variables or a secrets manager.",
351
+ envVar: "GITHUB_TOKEN",
352
+ secretType: "api-key"
353
+ },
354
+ {
355
+ name: "AWS access key",
356
+ pattern: /['"`](AKIA[A-Z0-9]{12,})['"`]/g,
357
+ secretGroup: 1,
358
+ help: "Move AWS credentials to environment variables, ~/.aws/credentials, or a secrets manager.",
359
+ envVar: "AWS_ACCESS_KEY_ID",
360
+ secretType: "aws-key"
361
+ },
362
+ {
363
+ name: "Google API key",
364
+ pattern: /['"`](AIza[A-Za-z0-9_-]{20,})['"`]/g,
365
+ secretGroup: 1,
366
+ help: "Move Google API keys to environment variables or a secrets manager.",
367
+ envVar: "GOOGLE_API_KEY",
368
+ secretType: "api-key"
369
+ },
370
+ {
371
+ name: "API key",
372
+ pattern: /['"`](key-[A-Za-z0-9_-]{10,})['"`]/g,
373
+ secretGroup: 1,
374
+ help: "Move API keys to environment variables or a secrets manager. Never commit secrets to source control.",
375
+ envVar: "SECRET_KEY",
376
+ secretType: "api-key"
377
+ },
378
+ {
379
+ name: "Bearer token",
380
+ pattern: /['"`](Bearer\s+[A-Za-z0-9_.-]{10,})['"`]/gi,
381
+ secretGroup: 1,
382
+ help: "Move bearer tokens to environment variables or a secrets manager.",
383
+ envVar: "AUTH_TOKEN",
384
+ secretType: "token"
385
+ },
386
+ {
387
+ name: "GitHub token",
388
+ pattern: /['"`](ghpt_[A-Za-z0-9_-]{10,})['"`]/g,
389
+ secretGroup: 1,
390
+ help: "Move GitHub tokens to environment variables or a secrets manager.",
391
+ envVar: "GITHUB_TOKEN",
392
+ secretType: "token"
393
+ },
394
+ {
395
+ name: "Password assignment",
396
+ pattern: /(?:password|pwd|passwd|pass)\s*(?:=|:)\s*['"`]([^'"`]{4,})['"`]/gi,
397
+ secretGroup: 1,
398
+ help: "Move passwords to environment variables or a secrets manager. Never commit credentials to source control.",
399
+ envVar: "PASSWORD",
400
+ secretType: "password"
401
+ },
402
+ {
403
+ name: "Secret assignment",
404
+ pattern: /(?:secret|token|apikey|api_key|access_key|private_key)\s*(?:=|:)\s*['"`]([^'"`]{4,})['"`]/gi,
405
+ secretGroup: 1,
406
+ help: "Move secrets to environment variables or a secrets manager. Never commit secrets to source control.",
407
+ envVar: "SECRET",
408
+ secretType: "secret"
409
+ },
410
+ {
411
+ name: "DB connection string",
412
+ pattern: /['"`]((?:mongodb|postgres|postgresql|mysql|redis|amqp|amqps):\/\/[^'"`]{8,})['"`]/gi,
413
+ secretGroup: 1,
414
+ help: "Move database connection strings to environment variables. Never commit credentials to source control.",
415
+ envVar: "DATABASE_URL",
416
+ secretType: "connection-string"
417
+ },
418
+ {
419
+ name: "PEM private key",
420
+ pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
421
+ secretGroup: 0,
422
+ help: "Move private keys to a secrets manager or secure file storage. Never commit private keys to source control.",
423
+ envVar: "PRIVATE_KEY_PATH",
424
+ secretType: "private-key"
425
+ }
426
+ ];
427
+ function detectSecrets(filePath, lines) {
428
+ const diagnostics = [];
429
+ if (isTestFile$1(filePath)) return diagnostics;
430
+ for (const { num, text } of lines) {
431
+ const trimmed = text.trim();
432
+ if (trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
433
+ for (const pattern of SECRET_PATTERNS) {
434
+ pattern.pattern.lastIndex = 0;
435
+ let match;
436
+ while ((match = pattern.pattern.exec(text)) !== null) {
437
+ const secret = match[pattern.secretGroup];
438
+ const col = match.index + 1;
439
+ const maskedValue = maskSecrets(secret || match[0]);
440
+ diagnostics.push(makeSecretDiagnostic(filePath, "security-deep/hardcoded-secret", "error", `Hardcoded ${pattern.name} detected: ${maskedValue}`, pattern.help, num, col, {
441
+ fixable: true,
442
+ suggestion: {
443
+ type: "replace",
444
+ text: `process.env.${pattern.envVar}`,
445
+ confidence: .9,
446
+ reason: "Hardcoded secrets in source code are exposed in version control; use environment variables."
447
+ },
448
+ detail: {
449
+ secretType: pattern.secretType,
450
+ secretPrefix: secret ? secret.slice(0, 4) : void 0,
451
+ patternName: pattern.name
452
+ }
453
+ }));
454
+ }
455
+ }
456
+ }
457
+ return diagnostics;
458
+ }
459
+
460
+ //#endregion
461
+ //#region src/security/html-safety.ts
462
+ function makeHtmlDiagnostic(filePath, rule, severity, message, help, line, column, opts) {
463
+ return {
464
+ filePath,
465
+ engine: "security-deep",
466
+ rule,
467
+ severity,
468
+ message,
469
+ help,
470
+ line,
471
+ column,
472
+ category: "security",
473
+ fixable: opts?.fixable ?? false,
474
+ suggestion: opts?.suggestion,
475
+ detail: opts?.detail
476
+ };
477
+ }
478
+ function isCommentLine(text) {
479
+ const trimmed = text.trim();
480
+ if (trimmed.startsWith("#")) return true;
481
+ if (trimmed.startsWith("//")) return true;
482
+ if (trimmed.startsWith("/*")) return true;
483
+ if (trimmed.startsWith("*")) return true;
484
+ return false;
485
+ }
486
+ function isJsxFile(filePath) {
487
+ return /\.(jsx|tsx)$/.test(filePath);
488
+ }
489
+ function isVueFile(filePath) {
490
+ return /\.vue$/.test(filePath);
491
+ }
492
+ const XSS_PATTERNS = [
493
+ {
494
+ name: "innerHTML assignment",
495
+ pattern: /\.innerHTML\s*=/,
496
+ rule: "security-deep/unsafe-html",
497
+ message: () => "Assignment to .innerHTML can lead to XSS if the value contains user input",
498
+ help: "Use textContent, innerText, or a sanitization library (e.g., DOMPurify) instead of innerHTML.",
499
+ suggestion: {
500
+ type: "replace",
501
+ text: ".textContent = ",
502
+ confidence: .6,
503
+ reason: "textContent safely sets text without interpreting HTML, preventing XSS."
504
+ },
505
+ detailName: "innerHTML"
506
+ },
507
+ {
508
+ name: "outerHTML assignment",
509
+ pattern: /\.outerHTML\s*=/,
510
+ rule: "security-deep/unsafe-html",
511
+ message: () => "Assignment to .outerHTML can lead to XSS if the value contains user input",
512
+ help: "Use DOM manipulation methods (createElement, appendChild) or a sanitization library instead of outerHTML.",
513
+ suggestion: {
514
+ type: "refactor",
515
+ text: "/* Replace outerHTML assignment with safe DOM manipulation */",
516
+ confidence: .6,
517
+ reason: "outerHTML replaces the entire element and can inject arbitrary HTML."
518
+ },
519
+ detailName: "outerHTML"
520
+ },
521
+ {
522
+ name: "document.write",
523
+ pattern: /\bdocument\s*\.\s*write\s*\(/,
524
+ rule: "security-deep/unsafe-html",
525
+ message: () => "document.write() can lead to XSS and breaks incremental rendering",
526
+ help: "Use DOM manipulation methods (createElement, appendChild) or framework rendering instead of document.write().",
527
+ suggestion: {
528
+ type: "refactor",
529
+ text: "/* Replace document.write() with safe DOM manipulation */",
530
+ confidence: .7,
531
+ reason: "document.write() can inject arbitrary HTML and overwrites the document if called after parse."
532
+ },
533
+ detailName: "document.write"
534
+ },
535
+ {
536
+ name: "document.writeln",
537
+ pattern: /\bdocument\s*\.\s*writeln\s*\(/,
538
+ rule: "security-deep/unsafe-html",
539
+ message: () => "document.writeln() can lead to XSS and breaks incremental rendering",
540
+ help: "Use DOM manipulation methods (createElement, appendChild) or framework rendering instead of document.writeln().",
541
+ suggestion: {
542
+ type: "refactor",
543
+ text: "/* Replace document.writeln() with safe DOM manipulation */",
544
+ confidence: .7,
545
+ reason: "document.writeln() has the same XSS risks as document.write()."
546
+ },
547
+ detailName: "document.writeln"
548
+ },
549
+ {
550
+ name: "dangerouslySetInnerHTML",
551
+ pattern: /dangerouslySetInnerHTML\s*=/,
552
+ rule: "security-deep/xss-risk",
553
+ message: () => "dangerouslySetInnerHTML renders raw HTML and can lead to XSS",
554
+ help: "Avoid dangerouslySetInnerHTML. Use React rendering or a sanitization library (e.g., DOMPurify) if raw HTML is required.",
555
+ suggestion: {
556
+ type: "refactor",
557
+ text: "/* Replace dangerouslySetInnerHTML with safe React rendering or sanitize with DOMPurify */",
558
+ confidence: .7,
559
+ reason: "dangerouslySetInnerHTML bypasses React's XSS protection by rendering raw HTML."
560
+ },
561
+ detailName: "dangerouslySetInnerHTML"
562
+ },
563
+ {
564
+ name: "v-html",
565
+ pattern: /v-html\s*=/,
566
+ rule: "security-deep/xss-risk",
567
+ message: () => "v-html renders raw HTML and can lead to XSS",
568
+ help: "Avoid v-html. Use Vue template rendering or a sanitization library (e.g., DOMPurify) if raw HTML is required.",
569
+ suggestion: {
570
+ type: "replace",
571
+ text: "v-text=",
572
+ confidence: .6,
573
+ reason: "v-text safely renders text content without interpreting HTML, preventing XSS."
574
+ },
575
+ detailName: "v-html"
576
+ },
577
+ {
578
+ name: "innerHTML with interpolation",
579
+ pattern: /\.innerHTML\s*=\s*`[^`]*\$\{/,
580
+ rule: "security-deep/xss-risk",
581
+ message: () => "innerHTML with template literal interpolation — high XSS risk",
582
+ help: "Never interpolate dynamic values into innerHTML. Use textContent or sanitize with DOMPurify.",
583
+ suggestion: {
584
+ type: "replace",
585
+ text: ".textContent = ",
586
+ confidence: .85,
587
+ reason: "Template literal interpolation in innerHTML directly injects untrusted values as HTML."
588
+ },
589
+ detailName: "innerHTML-interpolation"
590
+ },
591
+ {
592
+ name: "innerHTML with concatenation",
593
+ pattern: /\.innerHTML\s*=\s*['"`][^'"`]*['"`]\s*\+/,
594
+ rule: "security-deep/xss-risk",
595
+ message: () => "innerHTML with string concatenation — high XSS risk",
596
+ help: "Never concatenate dynamic values into innerHTML. Use textContent or sanitize with DOMPurify.",
597
+ suggestion: {
598
+ type: "replace",
599
+ text: ".textContent = ",
600
+ confidence: .85,
601
+ reason: "String concatenation in innerHTML directly injects untrusted values as HTML."
602
+ },
603
+ detailName: "innerHTML-concatenation"
604
+ },
605
+ {
606
+ name: "insertAdjacentHTML",
607
+ pattern: /\.insertAdjacentHTML\s*\(/,
608
+ rule: "security-deep/xss-risk",
609
+ message: () => "insertAdjacentHTML() renders raw HTML and can lead to XSS",
610
+ help: "Use insertAdjacentText() or safe DOM manipulation methods instead of insertAdjacentHTML().",
611
+ suggestion: {
612
+ type: "replace",
613
+ text: ".insertAdjacentText(",
614
+ confidence: .7,
615
+ reason: "insertAdjacentText safely inserts text without interpreting HTML, preventing XSS."
616
+ },
617
+ detailName: "insertAdjacentHTML"
618
+ },
619
+ {
620
+ name: "document.write with input",
621
+ pattern: /\bdocument\s*\.\s*write\s*\([^)]*\+/,
622
+ rule: "security-deep/xss-risk",
623
+ message: () => "document.write() with concatenated input — XSS risk",
624
+ help: "Avoid document.write() entirely. Use safe DOM manipulation with sanitized content.",
625
+ suggestion: {
626
+ type: "refactor",
627
+ text: "/* Replace document.write() with safe DOM manipulation */",
628
+ confidence: .8,
629
+ reason: "Concatenating user input into document.write() is a direct XSS vector."
630
+ },
631
+ detailName: "document.write-concatenation"
632
+ }
633
+ ];
634
+ function detectHtmlSafety(filePath, lines) {
635
+ const diagnostics = [];
636
+ for (const { num, text } of lines) {
637
+ if (isCommentLine(text)) continue;
638
+ for (const xssPattern of XSS_PATTERNS) {
639
+ const match = text.match(xssPattern.pattern);
640
+ if (!match) continue;
641
+ const col = text.indexOf(match[0]) + 1;
642
+ if ((text.slice(0, col - 1).match(/['"`]/g) ?? []).length % 2 === 1 && isCommentLine(text)) continue;
643
+ diagnostics.push(makeHtmlDiagnostic(filePath, xssPattern.rule, "error", xssPattern.message(match), xssPattern.help, num, col, {
644
+ fixable: false,
645
+ suggestion: xssPattern.suggestion,
646
+ detail: {
647
+ pattern: xssPattern.detailName,
648
+ fileContext: isJsxFile(filePath) ? "jsx" : isVueFile(filePath) ? "vue" : "generic"
649
+ }
650
+ }));
651
+ }
652
+ }
653
+ return diagnostics;
654
+ }
655
+
656
+ //#endregion
657
+ //#region src/engines/security-deep/index.ts
658
+ function makeDiagnostic(filePath, rule, severity, message, help, line, column, opts) {
659
+ return {
660
+ filePath,
661
+ engine: "security-deep",
662
+ rule,
663
+ severity,
664
+ message,
665
+ help,
666
+ line,
667
+ column,
668
+ category: "security",
669
+ fixable: opts?.fixable ?? false,
670
+ suggestion: opts?.suggestion,
671
+ detail: opts?.detail
672
+ };
673
+ }
674
+ /**
675
+ * Track whether we are inside a block comment (/* ... *​/) across lines.
676
+ * Returns { skip, inBlockComment } — if skip is true the entire line is a
677
+ * comment and should not be scanned for security patterns.
678
+ */
679
+ function checkCommentState(text, inBlockComment) {
680
+ const trimmed = text.trim();
681
+ if (inBlockComment) {
682
+ const closeIdx = text.indexOf("*/");
683
+ if (closeIdx === -1) return {
684
+ skip: true,
685
+ inBlockComment: true
686
+ };
687
+ const afterClose = text.substring(closeIdx + 2);
688
+ const reopenIdx = afterClose.indexOf("/*");
689
+ if (reopenIdx !== -1) return {
690
+ skip: true,
691
+ inBlockComment: afterClose.indexOf("*/", reopenIdx + 2) === -1
692
+ };
693
+ return {
694
+ skip: true,
695
+ inBlockComment: false
696
+ };
697
+ }
698
+ if (trimmed.startsWith("#")) return {
699
+ skip: true,
700
+ inBlockComment: false
701
+ };
702
+ if (trimmed.startsWith("//")) return {
703
+ skip: true,
704
+ inBlockComment: false
705
+ };
706
+ if (trimmed.startsWith("/*")) return {
707
+ skip: true,
708
+ inBlockComment: text.indexOf("*/", text.indexOf("/*") + 2) === -1
709
+ };
710
+ if (trimmed.startsWith("*")) {
711
+ const afterStar = trimmed.substring(1).trimStart();
712
+ if (/^[=;(]/.test(afterStar)) return {
713
+ skip: false,
714
+ inBlockComment: false
715
+ };
716
+ return {
717
+ skip: true,
718
+ inBlockComment: text.indexOf("*/") === -1
719
+ };
720
+ }
721
+ const openIdx = text.indexOf("/*");
722
+ if (openIdx !== -1) return {
723
+ skip: false,
724
+ inBlockComment: text.substring(openIdx + 2).indexOf("*/") === -1
725
+ };
726
+ return {
727
+ skip: false,
728
+ inBlockComment: false
729
+ };
730
+ }
731
+ /**
732
+ * Returns true when `matchStart` falls inside a string literal ('…"'/`…`/″…″)
733
+ * or a regex literal (/…/) on the given line of text.
734
+ *
735
+ * Heuristic:
736
+ * - Count unescaped single / double / backtick quotes before `matchStart`.
737
+ * If any count is odd the match sits inside that kind of string.
738
+ * - Look for regex-literal contexts (`= /`, `( /`, `, /`, `; /`, `[ /`,
739
+ * `return /`, etc.) and find the closing `/`. If the match is between
740
+ * the opening and closing `/` of a regex, it is inside the regex.
741
+ */
742
+ function isInsideStringOrRegex(text, matchStart) {
743
+ let sq = 0, dq = 0, bt = 0;
744
+ for (let i = 0; i < matchStart; i++) {
745
+ let bs = 0;
746
+ for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) bs++;
747
+ if (!(bs % 2 === 1)) {
748
+ if (text[i] === "'") sq++;
749
+ else if (text[i] === "\"") dq++;
750
+ else if (text[i] === "`") bt++;
751
+ }
752
+ }
753
+ if (sq % 2 === 1 || dq % 2 === 1 || bt % 2 === 1) return true;
754
+ const regexPrefix = /[=(:,;\[!&|?{}+\-~^%<> ]$/;
755
+ for (let i = 0; i < matchStart; i++) if (text[i] === "/" && i > 0 && regexPrefix.test(text[i - 1])) {
756
+ let j = i + 1;
757
+ for (; j < text.length; j++) {
758
+ if (text[j] === "\\") {
759
+ j++;
760
+ continue;
761
+ }
762
+ if (text[j] === "/") break;
763
+ if (text[j] === "[") {
764
+ for (j++; j < text.length && text[j] !== "]"; j++) if (text[j] === "\\") j++;
765
+ continue;
766
+ }
767
+ }
768
+ if (j < text.length && matchStart > i && matchStart < j) return true;
769
+ }
770
+ return false;
771
+ }
772
+ function isTestFile(filePath) {
773
+ const normalized = filePath.replace(/\\/g, "/");
774
+ if (normalized.endsWith(".test.ts") || normalized.endsWith(".spec.ts")) return true;
775
+ if (normalized.endsWith(".test.js") || normalized.endsWith(".spec.js")) return true;
776
+ if (/\/__tests__\//.test(normalized)) return true;
777
+ if (/(?:^|\/)(?:test|tests)\//.test(normalized)) return true;
778
+ return false;
779
+ }
780
+ function containsSQLKeyword(text) {
781
+ return /\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b/i.test(text);
782
+ }
783
+ function detectEvalUsage(filePath, lines) {
784
+ const diagnostics = [];
785
+ const evalRe = /\beval\s*\(/;
786
+ const newFunctionRe = /\bnew\s+Function\s*\(/;
787
+ const timerStringRe = /\b(setTimeout|setInterval)\s*\(\s*['"`]/;
788
+ let inBlockComment = false;
789
+ for (const { num, text } of lines) {
790
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
791
+ inBlockComment = newBlockState;
792
+ if (skip) continue;
793
+ const col = text.search(evalRe);
794
+ if (col !== -1) {
795
+ if (isInsideStringOrRegex(text, col)) continue;
796
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/eval-usage", "error", "Use of eval() allows arbitrary code execution", "Replace eval() with a safer alternative such as JSON.parse() for data, or refactor to avoid dynamic code execution.", num, col + 1, {
797
+ fixable: false,
798
+ suggestion: {
799
+ type: "refactor",
800
+ text: "/* Replace eval() with a safe alternative */",
801
+ confidence: .7,
802
+ reason: "eval() enables code injection attacks; use JSON.parse() for data or explicit logic for control flow."
803
+ }
804
+ }));
805
+ }
806
+ const nfCol = text.search(newFunctionRe);
807
+ if (nfCol !== -1) {
808
+ if (isInsideStringOrRegex(text, nfCol)) continue;
809
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/eval-usage", "error", "new Function() is equivalent to eval() and allows arbitrary code execution", "Avoid new Function(). Use closures, callbacks, or pre-defined functions instead.", num, nfCol + 1, {
810
+ fixable: false,
811
+ suggestion: {
812
+ type: "refactor",
813
+ text: "/* Replace new Function() with a pre-defined function */",
814
+ confidence: .7,
815
+ reason: "new Function() creates functions from strings at runtime, enabling injection attacks."
816
+ }
817
+ }));
818
+ }
819
+ const timerMatch = text.match(timerStringRe);
820
+ if (timerMatch) {
821
+ const timerCol = text.indexOf(timerMatch[1]);
822
+ if (isInsideStringOrRegex(text, timerCol)) continue;
823
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/eval-usage", "error", `${timerMatch[1]}() called with a string argument acts as eval()`, `Pass a function reference instead of a string to ${timerMatch[1]}().`, num, timerCol + 1, {
824
+ fixable: false,
825
+ suggestion: {
826
+ type: "replace",
827
+ text: `${timerMatch[1]}(() => { /* safe callback */ })`,
828
+ confidence: .8,
829
+ reason: "Passing a string to setTimeout/setInterval uses eval-like evaluation; use a function reference."
830
+ }
831
+ }));
832
+ }
833
+ }
834
+ return diagnostics;
835
+ }
836
+ function detectInnerHTML(filePath, lines) {
837
+ const diagnostics = [];
838
+ const innerHtmlRe = /\.innerHTML\s*=/;
839
+ const docWriteRe = /\bdocument\s*\.\s*write\s*\(/;
840
+ let inBlockComment = false;
841
+ for (const { num, text } of lines) {
842
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
843
+ inBlockComment = newBlockState;
844
+ if (skip) continue;
845
+ const ihCol = text.search(innerHtmlRe);
846
+ if (ihCol !== -1) {
847
+ if (isInsideStringOrRegex(text, ihCol)) continue;
848
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/inner-html", "error", "Assignment to .innerHTML can lead to XSS if the value contains user input", "Use textContent, innerText, or a sanitization library (e.g., DOMPurify) instead of innerHTML.", num, ihCol + 1, {
849
+ fixable: false,
850
+ suggestion: {
851
+ type: "replace",
852
+ text: ".textContent = ",
853
+ confidence: .6,
854
+ reason: "textContent safely sets text without interpreting HTML, preventing XSS."
855
+ }
856
+ }));
857
+ }
858
+ const dwCol = text.search(docWriteRe);
859
+ if (dwCol !== -1) {
860
+ if (isInsideStringOrRegex(text, dwCol)) continue;
861
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/inner-html", "error", "document.write() can lead to XSS and breaks incremental rendering", "Use DOM manipulation methods (createElement, appendChild) or framework rendering instead of document.write().", num, dwCol + 1, {
862
+ fixable: false,
863
+ suggestion: {
864
+ type: "refactor",
865
+ text: "/* Replace document.write() with safe DOM manipulation */",
866
+ confidence: .7,
867
+ reason: "document.write() can inject arbitrary HTML and overwrites the document if called after parse."
868
+ }
869
+ }));
870
+ }
871
+ }
872
+ return diagnostics;
873
+ }
874
+ function detectSQLInjection(filePath, lines) {
875
+ const diagnostics = [];
876
+ const sqlConcatRe = /\b(?:query|execute|raw|run|all|exec|execSql)\s*\(\s*['"`][^'"`]*['"`]\s*\+/;
877
+ const sqlTemplateRe = /\b(?:query|execute|raw|run|all|exec|execSql)\s*\(\s*`[^`]*\$\{/;
878
+ let inBlockComment = false;
879
+ for (const { num, text } of lines) {
880
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
881
+ inBlockComment = newBlockState;
882
+ if (skip) continue;
883
+ const concatCol = text.search(sqlConcatRe);
884
+ if (concatCol !== -1) {
885
+ if (isInsideStringOrRegex(text, concatCol)) continue;
886
+ if (!containsSQLKeyword(text)) continue;
887
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/sql-injection", "error", "String concatenation in SQL query detected — potential SQL injection", "Use parameterized queries or prepared statements instead of concatenating user input into SQL strings.", num, concatCol + 1, {
888
+ fixable: false,
889
+ suggestion: {
890
+ type: "refactor",
891
+ text: "/* Use parameterized query: query('SELECT * FROM users WHERE id = ?', [userId]) */",
892
+ confidence: .85,
893
+ reason: "Parameterized queries separate SQL logic from data, preventing injection."
894
+ }
895
+ }));
896
+ }
897
+ const tmplCol = text.search(sqlTemplateRe);
898
+ if (tmplCol !== -1) {
899
+ if (isInsideStringOrRegex(text, tmplCol)) continue;
900
+ if (!containsSQLKeyword(text)) continue;
901
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/sql-injection", "error", "Template literal interpolation in SQL query detected — potential SQL injection", "Use parameterized queries or prepared statements instead of interpolating values into SQL strings.", num, tmplCol + 1, {
902
+ fixable: false,
903
+ suggestion: {
904
+ type: "refactor",
905
+ text: "/* Use parameterized query: query('SELECT * FROM users WHERE id = ?', [userId]) */",
906
+ confidence: .85,
907
+ reason: "Template literals embed values directly into SQL; parameterized queries prevent injection."
908
+ }
909
+ }));
910
+ }
911
+ }
912
+ return diagnostics;
913
+ }
914
+ function detectShellInjection(filePath, lines) {
915
+ const diagnostics = [];
916
+ const execConcatRe = /\b(?:exec|execSync)\s*\(\s*['"`][^'"`]*['"`]\s*\+/;
917
+ const execTemplateRe = /\b(?:exec|execSync)\s*\(\s*`[^`]*\$\{/;
918
+ let inBlockComment = false;
919
+ for (const { num, text } of lines) {
920
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
921
+ inBlockComment = newBlockState;
922
+ if (skip) continue;
923
+ const concatCol = text.search(execConcatRe);
924
+ if (concatCol !== -1) {
925
+ if (isInsideStringOrRegex(text, concatCol)) continue;
926
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/shell-injection", "error", "String concatenation in exec/execSync detected — potential shell injection", "Use execFile/spawn with an array of arguments instead of exec with string concatenation.", num, concatCol + 1, {
927
+ fixable: false,
928
+ suggestion: {
929
+ type: "refactor",
930
+ text: "/* Use execFile or spawn with argument array: spawn('cmd', ['arg1', arg2]) */",
931
+ confidence: .85,
932
+ reason: "exec() passes strings to a shell; spawn/execFile avoid shell interpretation."
933
+ }
934
+ }));
935
+ }
936
+ const tmplCol = text.search(execTemplateRe);
937
+ if (tmplCol !== -1) {
938
+ if (isInsideStringOrRegex(text, tmplCol)) continue;
939
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/shell-injection", "error", "Template literal in exec/execSync detected — potential shell injection", "Use execFile/spawn with an array of arguments instead of exec with template literals.", num, tmplCol + 1, {
940
+ fixable: false,
941
+ suggestion: {
942
+ type: "refactor",
943
+ text: "/* Use execFile or spawn with argument array: spawn('cmd', ['arg1', arg2]) */",
944
+ confidence: .85,
945
+ reason: "exec() passes strings to a shell; spawn/execFile avoid shell interpretation."
946
+ }
947
+ }));
948
+ }
949
+ }
950
+ return diagnostics;
951
+ }
952
+ function detectPrototypePollution(filePath, lines) {
953
+ const diagnostics = [];
954
+ const objectAssignRe = /\bObject\s*\.\s*assign\s*\(\s*\w+\s*,\s*\w+/;
955
+ const protoRe = /__proto__/;
956
+ const deepMergeRe = /\b(?:deepMerge|deepExtend|mergeDeep|defaultsDeep)\s*\(/;
957
+ let inBlockComment = false;
958
+ for (const { num, text } of lines) {
959
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
960
+ inBlockComment = newBlockState;
961
+ if (skip) continue;
962
+ const oaCol = text.search(objectAssignRe);
963
+ if (oaCol !== -1) {
964
+ if (isInsideStringOrRegex(text, oaCol)) continue;
965
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/prototype-pollution", "warning", "Object.assign() with user-controlled source may enable prototype pollution", "Validate or sanitize the source object before merging, or use Object.assign with a fresh target: Object.assign({}, safeDefaults, userInput).", num, oaCol + 1, {
966
+ fixable: false,
967
+ suggestion: {
968
+ type: "refactor",
969
+ text: "Object.assign({}, safeDefaults, sanitizedInput)",
970
+ confidence: .6,
971
+ reason: "Using a fresh {} target and sanitizing input prevents prototype pollution via __proto__."
972
+ }
973
+ }));
974
+ }
975
+ const protoCol = text.search(protoRe);
976
+ if (protoCol !== -1) {
977
+ if (isInsideStringOrRegex(text, protoCol)) continue;
978
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/prototype-pollution", "warning", "Direct __proto__ access detected — potential prototype pollution vector", "Avoid __proto__ access. Use Object.getPrototypeOf() / Object.setPrototypeOf() or Map instead.", num, protoCol + 1, {
979
+ fixable: false,
980
+ suggestion: {
981
+ type: "replace",
982
+ text: "/* Use Object.getPrototypeOf() or Map instead of __proto__ */",
983
+ confidence: .7,
984
+ reason: "__proto__ can be leveraged for prototype pollution attacks when user input reaches it."
985
+ }
986
+ }));
987
+ }
988
+ const dmCol = text.search(deepMergeRe);
989
+ if (dmCol !== -1) {
990
+ if (isInsideStringOrRegex(text, dmCol)) continue;
991
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/prototype-pollution", "warning", "Deep merge function detected — ensure inputs are sanitized against prototype pollution", "Use a prototype-pollution-safe merge library or explicitly filter __proto__, constructor, and prototype keys.", num, dmCol + 1, {
992
+ fixable: false,
993
+ suggestion: {
994
+ type: "refactor",
995
+ text: "/* Use a safe merge that ignores __proto__, constructor, and prototype keys */",
996
+ confidence: .6,
997
+ reason: "Deep merge utilities can propagate __proto__ properties, leading to prototype pollution."
998
+ }
999
+ }));
1000
+ }
1001
+ }
1002
+ return diagnostics;
1003
+ }
1004
+ function detectSSRF(filePath, lines) {
1005
+ const diagnostics = [];
1006
+ const fetchVarRe = /\b(?:fetch|axios\s*\.\s*(?:get|post|put|delete|patch|request)|http\s*\.\s*request|https\s*\.\s*request|axios)\s*\(\s*(\w+)\s*[,)]/;
1007
+ const fetchTemplateRe = /\b(?:fetch|axios\s*\.\s*(?:get|post|put|delete|patch|request)|http\s*\.\s*request|https\s*\.\s*request)\s*\(\s*`[^`]*\$\{/;
1008
+ const userInputHints = /^(url|uri|href|link|redirect|callback|next|dest|target|returnTo|path|endpoint|address|domain|host|site|page|ref|referer|location|goto)$/i;
1009
+ let inBlockComment = false;
1010
+ for (const { num, text } of lines) {
1011
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
1012
+ inBlockComment = newBlockState;
1013
+ if (skip) continue;
1014
+ const fetchMatch = text.match(fetchVarRe);
1015
+ if (fetchMatch) {
1016
+ const varName = fetchMatch[1];
1017
+ const col = text.indexOf(varName);
1018
+ const isUserInputHint = userInputHints.test(varName);
1019
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/ssrf-risk", "warning", isUserInputHint ? `Potential SSRF: ${varName} appears to be user-controlled and is passed directly to a network request` : `Network request uses variable '${varName}' — verify it is not user-controlled to prevent SSRF`, "Validate and whitelist allowed URLs/domains before making the request. Use an allow-list rather than a deny-list.", num, col + 1, {
1020
+ fixable: false,
1021
+ suggestion: {
1022
+ type: "refactor",
1023
+ text: "/* Validate URL against an allow-list before making the request */",
1024
+ confidence: .5,
1025
+ reason: "User-controlled URLs can point to internal services (SSRF); allow-list validation prevents this."
1026
+ }
1027
+ }));
1028
+ }
1029
+ const tmplCol = text.search(fetchTemplateRe);
1030
+ if (tmplCol !== -1) diagnostics.push(makeDiagnostic(filePath, "security-deep/ssrf-risk", "warning", "Network request with interpolated URL detected — potential SSRF if input is user-controlled", "Validate and whitelist allowed URLs/domains before making the request. Avoid interpolating user input into URLs.", num, tmplCol + 1, {
1031
+ fixable: false,
1032
+ suggestion: {
1033
+ type: "refactor",
1034
+ text: "/* Build URL safely and validate against allow-list before request */",
1035
+ confidence: .55,
1036
+ reason: "Interpolated URLs may contain user input pointing to internal services (SSRF)."
1037
+ }
1038
+ }));
1039
+ }
1040
+ return diagnostics;
1041
+ }
1042
+ function redactSecret(secret) {
1043
+ if (secret.length <= 4) return "****";
1044
+ return secret.slice(0, 4) + "***";
1045
+ }
1046
+ function detectHardcodedSecret(filePath, lines) {
1047
+ const diagnostics = [];
1048
+ if (isTestFile(filePath)) return diagnostics;
1049
+ const apiKeyRe = /['"`](sk-[A-Za-z0-9_-]{10,}|key-[A-Za-z0-9_-]{10,}|AIza[A-Za-z0-9_-]{20,})['"`]/g;
1050
+ const passwordRe = /(?:password|pwd|passwd|pass)\s*(?:=|:)\s*['"`]([^'"`]{4,})['"`]/gi;
1051
+ const tokenRe = /['"`](Bearer\s+[A-Za-z0-9_.-]{10,}|ghpt_[A-Za-z0-9_-]{10,})['"`]/g;
1052
+ const awsKeyRe = /['"`](AKIA[A-Z0-9]{12,})['"`]/g;
1053
+ let inBlockComment = false;
1054
+ for (const { num, text } of lines) {
1055
+ const { skip, inBlockComment: newBlockState } = checkCommentState(text, inBlockComment);
1056
+ inBlockComment = newBlockState;
1057
+ if (skip) continue;
1058
+ let match;
1059
+ apiKeyRe.lastIndex = 0;
1060
+ while ((match = apiKeyRe.exec(text)) !== null) {
1061
+ const secret = match[1];
1062
+ const col = match.index + 1;
1063
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/hardcoded-secret", "error", `Hardcoded API key detected: ${redactSecret(secret)}`, "Move secrets to environment variables or a secrets manager. Never commit secrets to source control.", num, col, {
1064
+ fixable: true,
1065
+ suggestion: {
1066
+ type: "replace",
1067
+ text: `process.env.SECRET_KEY`,
1068
+ confidence: .9,
1069
+ reason: "Hardcoded secrets in source code are exposed in version control; use environment variables."
1070
+ },
1071
+ detail: {
1072
+ secretPrefix: secret.slice(0, 4),
1073
+ secretType: "api-key"
1074
+ }
1075
+ }));
1076
+ }
1077
+ passwordRe.lastIndex = 0;
1078
+ while ((match = passwordRe.exec(text)) !== null) {
1079
+ const secret = match[1];
1080
+ const col = match.index + 1;
1081
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/hardcoded-secret", "error", `Hardcoded password detected: ${redactSecret(secret)}`, "Move passwords to environment variables or a secrets manager. Never commit credentials to source control.", num, col, {
1082
+ fixable: true,
1083
+ suggestion: {
1084
+ type: "replace",
1085
+ text: `process.env.PASSWORD`,
1086
+ confidence: .9,
1087
+ reason: "Hardcoded passwords in source code are exposed in version control; use environment variables."
1088
+ },
1089
+ detail: {
1090
+ secretPrefix: secret.slice(0, 4),
1091
+ secretType: "password"
1092
+ }
1093
+ }));
1094
+ }
1095
+ tokenRe.lastIndex = 0;
1096
+ while ((match = tokenRe.exec(text)) !== null) {
1097
+ const secret = match[1];
1098
+ const col = match.index + 1;
1099
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/hardcoded-secret", "error", `Hardcoded token detected: ${redactSecret(secret)}`, "Move tokens to environment variables or a secrets manager. Never commit tokens to source control.", num, col, {
1100
+ fixable: true,
1101
+ suggestion: {
1102
+ type: "replace",
1103
+ text: `process.env.AUTH_TOKEN`,
1104
+ confidence: .9,
1105
+ reason: "Hardcoded tokens in source code are exposed in version control; use environment variables."
1106
+ },
1107
+ detail: {
1108
+ secretPrefix: secret.slice(0, 4),
1109
+ secretType: "token"
1110
+ }
1111
+ }));
1112
+ }
1113
+ awsKeyRe.lastIndex = 0;
1114
+ while ((match = awsKeyRe.exec(text)) !== null) {
1115
+ const secret = match[1];
1116
+ const col = match.index + 1;
1117
+ diagnostics.push(makeDiagnostic(filePath, "security-deep/hardcoded-secret", "error", `Hardcoded AWS access key detected: ${redactSecret(secret)}`, "Move AWS credentials to environment variables, ~/.aws/credentials, or a secrets manager.", num, col, {
1118
+ fixable: true,
1119
+ suggestion: {
1120
+ type: "replace",
1121
+ text: `process.env.AWS_ACCESS_KEY_ID`,
1122
+ confidence: .95,
1123
+ reason: "AWS keys in source code are a critical leak risk; use IAM roles or env vars."
1124
+ },
1125
+ detail: {
1126
+ secretPrefix: secret.slice(0, 4),
1127
+ secretType: "aws-key"
1128
+ }
1129
+ }));
1130
+ }
1131
+ }
1132
+ return diagnostics;
1133
+ }
1134
+ /**
1135
+ * Security vulnerability detection engine.
1136
+ *
1137
+ * Detects: eval usage, innerHTML/XSS, SQL injection, shell injection,
1138
+ * prototype pollution, SSRF risk, and hardcoded secrets.
1139
+ */
1140
+ const securityDeepEngine = {
1141
+ name: "security-deep",
1142
+ description: "Security vulnerability detection: eval, innerHTML, SQL injection, shell injection, prototype pollution, SSRF, hardcoded secrets, XSS risk, dependency audit",
1143
+ supportedLanguages: [
1144
+ "typescript",
1145
+ "javascript",
1146
+ "python",
1147
+ "go",
1148
+ "rust",
1149
+ "ruby",
1150
+ "php",
1151
+ "java"
1152
+ ],
1153
+ async run(context) {
1154
+ const start = performance.now();
1155
+ const diagnostics = [];
1156
+ const files = context.files ?? [];
1157
+ const rootDir = context.rootDirectory;
1158
+ const config = context.config;
1159
+ for (const filePath of files) try {
1160
+ const lines = toLines(await readFileContent(filePath));
1161
+ diagnostics.push(...detectEvalUsage(filePath, lines));
1162
+ diagnostics.push(...detectInnerHTML(filePath, lines));
1163
+ diagnostics.push(...detectSQLInjection(filePath, lines));
1164
+ diagnostics.push(...detectShellInjection(filePath, lines));
1165
+ diagnostics.push(...detectPrototypePollution(filePath, lines));
1166
+ diagnostics.push(...detectSSRF(filePath, lines));
1167
+ diagnostics.push(...detectHardcodedSecret(filePath, lines));
1168
+ diagnostics.push(...detectSecrets(filePath, lines));
1169
+ diagnostics.push(...detectHtmlSafety(filePath, lines));
1170
+ } catch {}
1171
+ if (config.security?.audit) {
1172
+ const auditTimeout = config.security?.auditTimeout ?? 25e3;
1173
+ if (context.languages.includes("typescript") || context.languages.includes("javascript")) diagnostics.push(...npmAudit(rootDir, auditTimeout));
1174
+ if (context.languages.includes("python")) diagnostics.push(...pipAudit(rootDir, auditTimeout));
1175
+ if (context.languages.includes("go")) diagnostics.push(...goVulnCheck(rootDir, auditTimeout));
1176
+ if (context.languages.includes("rust")) diagnostics.push(...cargoAudit(rootDir, auditTimeout));
1177
+ }
1178
+ const elapsed = performance.now() - start;
1179
+ return {
1180
+ engine: this.name,
1181
+ diagnostics,
1182
+ elapsed,
1183
+ skipped: false
1184
+ };
1185
+ },
1186
+ async fix(diagnostics, _context) {
1187
+ const fixable = diagnostics.filter((d) => d.rule === "security-deep/hardcoded-secret" && d.fixable);
1188
+ const remaining = diagnostics.filter((d) => d.rule !== "security-deep/hardcoded-secret" || !d.fixable);
1189
+ return {
1190
+ fixed: fixable.length,
1191
+ remaining,
1192
+ modifiedFiles: [...new Set(fixable.map((d) => d.filePath))]
1193
+ };
1194
+ }
1195
+ };
1196
+
1197
+ //#endregion
1198
+ export { securityDeepEngine };