@yuaone/core 0.1.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.
Files changed (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,1129 @@
1
+ /**
2
+ * @module security-scanner
3
+ * @description YUAN Security DAST (Dynamic Application Security Testing) module.
4
+ *
5
+ * Performs dynamic security analysis on code changes:
6
+ * 1. Dependency Vulnerability Scanning — known CVE patterns in package.json
7
+ * 2. Secret Detection — API keys, tokens, passwords, private keys
8
+ * 3. Code Security Patterns — injection, XSS, SSRF, traversal, crypto
9
+ * 4. Configuration Security — tsconfig, CSP, CORS, headers
10
+ * 5. Report Generation — severity-based findings with pass/fail decision
11
+ *
12
+ * No external dependencies — Node builtins only.
13
+ * All secrets in evidence are masked.
14
+ */
15
+ import { readFile, readdir, stat } from "node:fs/promises";
16
+ import { join, relative, extname } from "node:path";
17
+ import { randomUUID } from "node:crypto";
18
+ // ══════════════════════════════════════════════════════════════════════
19
+ // Constants — Severity ordering
20
+ // ══════════════════════════════════════════════════════════════════════
21
+ const SEVERITY_ORDER = {
22
+ critical: 5,
23
+ high: 4,
24
+ medium: 3,
25
+ low: 2,
26
+ info: 1,
27
+ };
28
+ // ══════════════════════════════════════════════════════════════════════
29
+ // Built-in Patterns
30
+ // ══════════════════════════════════════════════════════════════════════
31
+ /** Secret detection patterns */
32
+ const SECRET_PATTERNS = [
33
+ {
34
+ name: "aws-access-key",
35
+ regex: /\bAKIA[0-9A-Z]{16}\b/,
36
+ severity: "critical",
37
+ category: "secret",
38
+ message: "AWS Access Key ID detected",
39
+ suggestion: "Remove the key and rotate it immediately. Use environment variables or AWS IAM roles.",
40
+ },
41
+ {
42
+ name: "aws-secret-key",
43
+ regex: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/,
44
+ severity: "medium",
45
+ category: "secret",
46
+ message: "Possible AWS Secret Access Key detected (40-char base64 string)",
47
+ suggestion: "Verify if this is a secret key. Use environment variables instead of hardcoding.",
48
+ },
49
+ {
50
+ name: "github-token",
51
+ regex: /\b(ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})\b/,
52
+ severity: "critical",
53
+ category: "secret",
54
+ message: "GitHub token detected",
55
+ suggestion: "Remove the token and revoke it. Use GITHUB_TOKEN environment variable.",
56
+ },
57
+ {
58
+ name: "generic-api-key",
59
+ regex: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/i,
60
+ severity: "high",
61
+ category: "secret",
62
+ message: "Hardcoded API key detected",
63
+ suggestion: "Move API keys to environment variables or a secrets manager.",
64
+ },
65
+ {
66
+ name: "generic-password",
67
+ regex: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/i,
68
+ severity: "high",
69
+ category: "secret",
70
+ message: "Hardcoded password or secret detected",
71
+ suggestion: "Never hardcode passwords. Use environment variables or a secrets manager.",
72
+ },
73
+ {
74
+ name: "private-key",
75
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
76
+ severity: "critical",
77
+ category: "secret",
78
+ message: "Private key detected in source code",
79
+ suggestion: "Remove the private key immediately. Store keys in a secure vault.",
80
+ },
81
+ {
82
+ name: "jwt-token",
83
+ regex: /\beyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\b/,
84
+ severity: "high",
85
+ category: "secret",
86
+ message: "JWT token detected in source code",
87
+ suggestion: "Remove hardcoded JWT tokens. Generate them at runtime.",
88
+ },
89
+ {
90
+ name: "slack-token",
91
+ regex: /\bxox[bpors]-[a-zA-Z0-9-]{10,}\b/,
92
+ severity: "critical",
93
+ category: "secret",
94
+ message: "Slack token detected",
95
+ suggestion: "Remove the token and revoke it. Use environment variables.",
96
+ },
97
+ {
98
+ name: "stripe-key",
99
+ regex: /\b[sr]k_(live|test)_[a-zA-Z0-9]{20,}\b/,
100
+ severity: "critical",
101
+ category: "secret",
102
+ message: "Stripe API key detected",
103
+ suggestion: "Remove the key and rotate it. Use environment variables.",
104
+ },
105
+ {
106
+ name: "google-api-key",
107
+ regex: /\bAIza[a-zA-Z0-9_-]{35}\b/,
108
+ severity: "high",
109
+ category: "secret",
110
+ message: "Google API key detected",
111
+ suggestion: "Remove the key and restrict it. Use environment variables.",
112
+ },
113
+ {
114
+ name: "env-file-reference",
115
+ regex: /\.env(?:\.local|\.production|\.staging|\.development)?$/,
116
+ severity: "medium",
117
+ category: "secret",
118
+ message: ".env file should not be committed to version control",
119
+ suggestion: "Add .env files to .gitignore. Use .env.example for templates.",
120
+ },
121
+ {
122
+ name: "base64-secret",
123
+ regex: /(?:secret|token|key|password|credential)\s*[:=]\s*["'](?:[A-Za-z0-9+/]{32,}={0,2})["']/i,
124
+ severity: "medium",
125
+ category: "secret",
126
+ message: "Possible base64-encoded secret detected",
127
+ suggestion: "Verify if this is a secret. Use environment variables for sensitive data.",
128
+ },
129
+ {
130
+ name: "connection-string",
131
+ regex: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^"'\s]+:[^"'\s]+@/i,
132
+ severity: "high",
133
+ category: "secret",
134
+ message: "Database connection string with credentials detected",
135
+ suggestion: "Use environment variables for connection strings. Never hardcode credentials.",
136
+ },
137
+ ];
138
+ /** SQL injection patterns */
139
+ const INJECTION_PATTERNS = [
140
+ {
141
+ name: "sql-string-concat",
142
+ regex: /(?:query|execute|exec|raw)\s*\(\s*(?:["'`].*?\b(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b.*?["'`]\s*\+|`[^`]*\$\{)/i,
143
+ severity: "high",
144
+ category: "injection",
145
+ message: "Possible SQL injection via string concatenation or template literal",
146
+ suggestion: "Use parameterized queries or an ORM. Never concatenate user input into SQL.",
147
+ },
148
+ {
149
+ name: "sql-template-literal",
150
+ regex: /\b(?:query|execute|exec|raw)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*(?:WHERE|AND|OR|SET|VALUES)\b/i,
151
+ severity: "high",
152
+ category: "injection",
153
+ message: "SQL query with template literal interpolation",
154
+ suggestion: "Use parameterized queries. Template literals in SQL are injection vectors.",
155
+ },
156
+ {
157
+ name: "command-injection-exec",
158
+ regex: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:.*?\+|`[^`]*\$\{)/,
159
+ severity: "critical",
160
+ category: "injection",
161
+ message: "Possible command injection via exec/spawn with dynamic input",
162
+ suggestion: "Use execFile with argument arrays. Never pass user input to exec().",
163
+ },
164
+ {
165
+ name: "command-injection-shell",
166
+ regex: /\bchild_process\b.*?\bexec\s*\(/,
167
+ severity: "high",
168
+ category: "injection",
169
+ message: "Use of child_process.exec which runs in a shell",
170
+ suggestion: "Prefer execFile() over exec() to avoid shell interpretation.",
171
+ },
172
+ {
173
+ name: "eval-usage",
174
+ regex: /\beval\s*\(\s*(?!["'`])/,
175
+ severity: "critical",
176
+ category: "injection",
177
+ message: "Use of eval() with potentially dynamic input",
178
+ suggestion: "Avoid eval(). Use JSON.parse() for data, or Function constructor with caution.",
179
+ },
180
+ {
181
+ name: "new-function",
182
+ regex: /new\s+Function\s*\(\s*(?:.*?\+|`[^`]*\$\{)/,
183
+ severity: "high",
184
+ category: "injection",
185
+ message: "Dynamic Function constructor with interpolated input",
186
+ suggestion: "Avoid new Function() with dynamic input. Use safer alternatives.",
187
+ },
188
+ ];
189
+ /** XSS patterns */
190
+ const XSS_PATTERNS = [
191
+ {
192
+ name: "innerhtml-assignment",
193
+ regex: /\.innerHTML\s*=\s*(?!["'`]\s*$)/,
194
+ severity: "high",
195
+ category: "xss",
196
+ message: "Direct innerHTML assignment — potential XSS vector",
197
+ suggestion: "Use textContent for text or a sanitization library (DOMPurify) for HTML.",
198
+ },
199
+ {
200
+ name: "dangerously-set-innerhtml",
201
+ regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!.*sanitize|.*DOMPurify|.*purify)/i,
202
+ severity: "high",
203
+ category: "xss",
204
+ message: "dangerouslySetInnerHTML without apparent sanitization",
205
+ suggestion: "Always sanitize HTML with DOMPurify before using dangerouslySetInnerHTML.",
206
+ },
207
+ {
208
+ name: "document-write",
209
+ regex: /document\.write\s*\(/,
210
+ severity: "medium",
211
+ category: "xss",
212
+ message: "document.write() usage — potential XSS and performance issues",
213
+ suggestion: "Use DOM manipulation methods instead of document.write().",
214
+ },
215
+ {
216
+ name: "outerhtml-assignment",
217
+ regex: /\.outerHTML\s*=/,
218
+ severity: "medium",
219
+ category: "xss",
220
+ message: "outerHTML assignment — potential XSS vector",
221
+ suggestion: "Use DOM manipulation methods. Sanitize input if HTML is required.",
222
+ },
223
+ {
224
+ name: "jquery-html",
225
+ regex: /\$\s*\([^)]*\)\s*\.html\s*\(\s*(?!["'`]\s*\))/,
226
+ severity: "medium",
227
+ category: "xss",
228
+ message: "jQuery .html() with potentially unsanitized input",
229
+ suggestion: "Use .text() for text content or sanitize HTML input.",
230
+ },
231
+ ];
232
+ /** Path traversal patterns */
233
+ const TRAVERSAL_PATTERNS = [
234
+ {
235
+ name: "path-traversal-user-input",
236
+ regex: /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|unlink|unlinkSync|stat|statSync|access|accessSync)\s*\(\s*(?:.*?\+|`[^`]*\$\{|.*?req\.|.*?params\.|.*?query\.|.*?body\.)/,
237
+ severity: "high",
238
+ category: "traversal",
239
+ message: "File operation with potentially user-controlled path",
240
+ suggestion: "Validate and sanitize file paths. Use path.resolve() and verify the result stays within allowed directories.",
241
+ },
242
+ {
243
+ name: "path-join-user-input",
244
+ regex: /path\.(?:join|resolve)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/,
245
+ severity: "medium",
246
+ category: "traversal",
247
+ message: "path.join/resolve with user-controlled input",
248
+ suggestion: "Validate that resolved paths stay within expected directories. Check for '..' sequences.",
249
+ },
250
+ ];
251
+ /** Insecure crypto patterns */
252
+ const CRYPTO_PATTERNS = [
253
+ {
254
+ name: "md5-usage",
255
+ regex: /\bcreateHash\s*\(\s*["']md5["']\s*\)/,
256
+ severity: "medium",
257
+ category: "crypto",
258
+ message: "MD5 hash detected — cryptographically weak",
259
+ suggestion: "Use SHA-256 or SHA-3 for security purposes. MD5 is only suitable for checksums.",
260
+ },
261
+ {
262
+ name: "sha1-usage",
263
+ regex: /\bcreateHash\s*\(\s*["']sha1["']\s*\)/,
264
+ severity: "medium",
265
+ category: "crypto",
266
+ message: "SHA-1 hash detected — deprecated for security",
267
+ suggestion: "Use SHA-256 or SHA-3. SHA-1 is deprecated for security purposes.",
268
+ },
269
+ {
270
+ name: "weak-random",
271
+ regex: /Math\.random\s*\(\s*\)/,
272
+ severity: "low",
273
+ category: "crypto",
274
+ message: "Math.random() is not cryptographically secure",
275
+ suggestion: "Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values.",
276
+ },
277
+ {
278
+ name: "hardcoded-iv",
279
+ regex: /createCipheriv\s*\([^,]+,\s*[^,]+,\s*(?:Buffer\.from\s*\(\s*["']|["'])/,
280
+ severity: "high",
281
+ category: "crypto",
282
+ message: "Hardcoded initialization vector (IV) in cipher",
283
+ suggestion: "Generate a random IV with crypto.randomBytes() for each encryption operation.",
284
+ },
285
+ ];
286
+ /** SSRF patterns */
287
+ const SSRF_PATTERNS = [
288
+ {
289
+ name: "ssrf-fetch",
290
+ regex: /\bfetch\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
291
+ severity: "high",
292
+ category: "ssrf",
293
+ message: "fetch() with potentially user-controlled URL — SSRF risk",
294
+ suggestion: "Validate and whitelist URLs before fetching. Block internal/private IP ranges.",
295
+ },
296
+ {
297
+ name: "ssrf-http-request",
298
+ regex: /\b(?:http|https)\.(?:get|request)\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
299
+ severity: "high",
300
+ category: "ssrf",
301
+ message: "HTTP request with potentially user-controlled URL — SSRF risk",
302
+ suggestion: "Validate and whitelist URLs. Block requests to internal networks (127.0.0.1, 10.x, 172.16.x, 192.168.x).",
303
+ },
304
+ {
305
+ name: "ssrf-axios",
306
+ regex: /\baxios\s*\.?\s*(?:get|post|put|patch|delete|request)\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
307
+ severity: "high",
308
+ category: "ssrf",
309
+ message: "Axios request with potentially user-controlled URL — SSRF risk",
310
+ suggestion: "Validate and whitelist URLs before making requests.",
311
+ },
312
+ ];
313
+ /** Prototype pollution patterns */
314
+ const POLLUTION_PATTERNS = [
315
+ {
316
+ name: "prototype-pollution-merge",
317
+ regex: /(?:Object\.assign|_\.merge|_\.defaultsDeep|_\.set|_\.setWith)\s*\(\s*(?:.*?req\.|.*?body\.|.*?params\.)/,
318
+ severity: "high",
319
+ category: "injection",
320
+ message: "Object merge with user input — prototype pollution risk",
321
+ suggestion: "Validate keys before merging. Block '__proto__', 'constructor', and 'prototype' keys.",
322
+ },
323
+ {
324
+ name: "bracket-notation-user-input",
325
+ regex: /\[[^\]]*(?:req\.|params\.|query\.|body\.)[^\]]*\]\s*=/,
326
+ severity: "medium",
327
+ category: "injection",
328
+ message: "Bracket notation assignment with user input — prototype pollution risk",
329
+ suggestion: "Validate property names. Block '__proto__', 'constructor', and 'prototype'.",
330
+ },
331
+ ];
332
+ /** Known vulnerable package patterns (subset of well-known CVEs) */
333
+ const VULNERABLE_PACKAGES = [
334
+ {
335
+ name: "lodash",
336
+ vulnerableVersions: "<4.17.21",
337
+ severity: "high",
338
+ cve: "CVE-2021-23337",
339
+ message: "lodash < 4.17.21 has prototype pollution vulnerabilities",
340
+ suggestion: "Upgrade lodash to >= 4.17.21.",
341
+ },
342
+ {
343
+ name: "minimist",
344
+ vulnerableVersions: "<1.2.6",
345
+ severity: "high",
346
+ cve: "CVE-2021-44906",
347
+ message: "minimist < 1.2.6 has prototype pollution vulnerability",
348
+ suggestion: "Upgrade minimist to >= 1.2.6.",
349
+ },
350
+ {
351
+ name: "node-forge",
352
+ vulnerableVersions: "<1.3.0",
353
+ severity: "high",
354
+ cve: "CVE-2022-24771",
355
+ message: "node-forge < 1.3.0 has signature verification bypass",
356
+ suggestion: "Upgrade node-forge to >= 1.3.0.",
357
+ },
358
+ {
359
+ name: "jsonwebtoken",
360
+ vulnerableVersions: "<9.0.0",
361
+ severity: "high",
362
+ cve: "CVE-2022-23529",
363
+ message: "jsonwebtoken < 9.0.0 has insecure key retrieval vulnerability",
364
+ suggestion: "Upgrade jsonwebtoken to >= 9.0.0.",
365
+ },
366
+ {
367
+ name: "express",
368
+ vulnerableVersions: "<4.19.2",
369
+ severity: "medium",
370
+ cve: "CVE-2024-29041",
371
+ message: "express < 4.19.2 has open redirect vulnerability",
372
+ suggestion: "Upgrade express to >= 4.19.2.",
373
+ },
374
+ {
375
+ name: "axios",
376
+ vulnerableVersions: "<1.6.0",
377
+ severity: "medium",
378
+ cve: "CVE-2023-45857",
379
+ message: "axios < 1.6.0 has CSRF vulnerability via cookie exposure",
380
+ suggestion: "Upgrade axios to >= 1.6.0.",
381
+ },
382
+ {
383
+ name: "semver",
384
+ vulnerableVersions: "<7.5.2",
385
+ severity: "medium",
386
+ cve: "CVE-2022-25883",
387
+ message: "semver < 7.5.2 has ReDoS vulnerability",
388
+ suggestion: "Upgrade semver to >= 7.5.2.",
389
+ },
390
+ {
391
+ name: "tar",
392
+ vulnerableVersions: "<6.2.1",
393
+ severity: "high",
394
+ cve: "CVE-2024-28863",
395
+ message: "tar < 6.2.1 has denial of service vulnerability",
396
+ suggestion: "Upgrade tar to >= 6.2.1.",
397
+ },
398
+ {
399
+ name: "xml2js",
400
+ vulnerableVersions: "<0.5.0",
401
+ severity: "medium",
402
+ cve: "CVE-2023-0842",
403
+ message: "xml2js < 0.5.0 has prototype pollution vulnerability",
404
+ suggestion: "Upgrade xml2js to >= 0.5.0.",
405
+ },
406
+ {
407
+ name: "tough-cookie",
408
+ vulnerableVersions: "<4.1.3",
409
+ severity: "medium",
410
+ cve: "CVE-2023-26136",
411
+ message: "tough-cookie < 4.1.3 has prototype pollution vulnerability",
412
+ suggestion: "Upgrade tough-cookie to >= 4.1.3.",
413
+ },
414
+ ];
415
+ /** Deprecated/unmaintained packages */
416
+ const DEPRECATED_PACKAGES = [
417
+ {
418
+ name: "request",
419
+ severity: "low",
420
+ message: "'request' package is deprecated and unmaintained",
421
+ suggestion: "Migrate to 'node-fetch', 'axios', or native fetch().",
422
+ },
423
+ {
424
+ name: "querystring",
425
+ severity: "info",
426
+ message: "'querystring' module is deprecated in Node.js",
427
+ suggestion: "Use URLSearchParams or the 'qs' package.",
428
+ },
429
+ {
430
+ name: "uuid",
431
+ vulnerableVersions: "<9.0.0",
432
+ severity: "info",
433
+ message: "Consider using Node.js built-in crypto.randomUUID() instead of uuid package",
434
+ suggestion: "Use crypto.randomUUID() (Node 19+) or keep uuid >= 9.0.0.",
435
+ },
436
+ ];
437
+ // ══════════════════════════════════════════════════════════════════════
438
+ // File extensions to scan
439
+ // ══════════════════════════════════════════════════════════════════════
440
+ const SCANNABLE_EXTENSIONS = new Set([
441
+ ".ts",
442
+ ".tsx",
443
+ ".js",
444
+ ".jsx",
445
+ ".mjs",
446
+ ".cjs",
447
+ ".json",
448
+ ".yaml",
449
+ ".yml",
450
+ ".toml",
451
+ ".env",
452
+ ".sh",
453
+ ".bash",
454
+ ".py",
455
+ ".go",
456
+ ".rs",
457
+ ".java",
458
+ ".rb",
459
+ ".php",
460
+ ".vue",
461
+ ".svelte",
462
+ ]);
463
+ const DEFAULT_IGNORE_PATHS = [
464
+ "node_modules",
465
+ ".git",
466
+ "dist",
467
+ "build",
468
+ "coverage",
469
+ ".next",
470
+ ".nuxt",
471
+ "__pycache__",
472
+ ".cache",
473
+ "vendor",
474
+ ];
475
+ // ══════════════════════════════════════════════════════════════════════
476
+ // SecurityScanner
477
+ // ══════════════════════════════════════════════════════════════════════
478
+ /**
479
+ * DAST Security Scanner — performs dynamic security analysis on code changes.
480
+ *
481
+ * Scans for secrets, injection vulnerabilities, XSS, SSRF, insecure crypto,
482
+ * dependency vulnerabilities, and configuration issues.
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * const scanner = new SecurityScanner({
487
+ * projectPath: '/path/to/project',
488
+ * scanSecrets: true,
489
+ * scanCode: true,
490
+ * });
491
+ * const result = await scanner.scan();
492
+ * console.log(result.summary);
493
+ * ```
494
+ */
495
+ export class SecurityScanner {
496
+ config;
497
+ constructor(config) {
498
+ this.config = {
499
+ projectPath: config.projectPath,
500
+ scanSecrets: config.scanSecrets ?? true,
501
+ scanDependencies: config.scanDependencies ?? true,
502
+ scanCode: config.scanCode ?? true,
503
+ scanConfig: config.scanConfig ?? true,
504
+ severityThreshold: config.severityThreshold ?? "low",
505
+ customPatterns: config.customPatterns ?? [],
506
+ ignorePaths: config.ignorePaths ?? [],
507
+ ignoreRules: config.ignoreRules ?? [],
508
+ };
509
+ }
510
+ // ────────────────────────────────────────────────────────────────────
511
+ // Full scan
512
+ // ────────────────────────────────────────────────────────────────────
513
+ /**
514
+ * Run a full security scan on the project.
515
+ *
516
+ * @param files Optional list of specific file paths to scan (relative to projectPath).
517
+ * If omitted, discovers files recursively.
518
+ * @returns Scan result with findings, summary, and pass/fail decision.
519
+ */
520
+ async scan(files) {
521
+ const start = Date.now();
522
+ const findings = [];
523
+ let filesScanned = 0;
524
+ // Discover or use provided files
525
+ const filePaths = files ?? (await this.discoverFiles(this.config.projectPath));
526
+ // Collect file contents
527
+ const fileContents = new Map();
528
+ for (const fp of filePaths) {
529
+ if (this.isIgnored(fp))
530
+ continue;
531
+ try {
532
+ const absPath = fp.startsWith("/") ? fp : join(this.config.projectPath, fp);
533
+ const content = await readFile(absPath, "utf-8");
534
+ const relPath = fp.startsWith("/")
535
+ ? relative(this.config.projectPath, fp)
536
+ : fp;
537
+ fileContents.set(relPath, content);
538
+ filesScanned++;
539
+ }
540
+ catch {
541
+ // Skip unreadable files
542
+ }
543
+ }
544
+ // Run scans
545
+ for (const [filePath, content] of fileContents) {
546
+ if (this.config.scanSecrets) {
547
+ findings.push(...this.scanFileForSecrets(content, filePath));
548
+ }
549
+ if (this.config.scanCode) {
550
+ findings.push(...this.scanFileForInjection(content, filePath));
551
+ findings.push(...this.scanFileForXSS(content, filePath));
552
+ findings.push(...this.scanFileForTraversal(content, filePath));
553
+ findings.push(...this.scanFileForCrypto(content, filePath));
554
+ findings.push(...this.scanFileForSSRF(content, filePath));
555
+ findings.push(...this.scanFileForPrototypePollution(content, filePath));
556
+ findings.push(...this.scanFileWithCustomPatterns(content, filePath));
557
+ }
558
+ // Dependency scan for package.json files
559
+ if (this.config.scanDependencies && filePath.endsWith("package.json")) {
560
+ findings.push(...this.scanDependencies(content));
561
+ }
562
+ }
563
+ // Config scan
564
+ if (this.config.scanConfig) {
565
+ findings.push(...this.scanConfig(fileContents));
566
+ }
567
+ // Filter by threshold and ignored rules
568
+ const filtered = findings.filter((f) => {
569
+ if (this.config.ignoreRules.includes(f.rule))
570
+ return false;
571
+ return SEVERITY_ORDER[f.severity] >= SEVERITY_ORDER[this.config.severityThreshold];
572
+ });
573
+ // Build summary
574
+ const summary = {
575
+ critical: 0,
576
+ high: 0,
577
+ medium: 0,
578
+ low: 0,
579
+ info: 0,
580
+ total: filtered.length,
581
+ };
582
+ for (const f of filtered) {
583
+ summary[f.severity]++;
584
+ }
585
+ // Pass/fail: no critical or high findings
586
+ const passed = summary.critical === 0 && summary.high === 0;
587
+ return {
588
+ findings: filtered,
589
+ summary,
590
+ passed,
591
+ scanDuration: Date.now() - start,
592
+ filesScanned,
593
+ };
594
+ }
595
+ // ────────────────────────────────────────────────────────────────────
596
+ // Individual scans — Secrets
597
+ // ────────────────────────────────────────────────────────────────────
598
+ /**
599
+ * Scan file contents for hardcoded secrets, API keys, and tokens.
600
+ *
601
+ * @param content File content to scan
602
+ * @param filePath Relative file path for reporting
603
+ * @returns Array of findings
604
+ */
605
+ scanFileForSecrets(content, filePath) {
606
+ // Check if this is a .env file being scanned (the file itself is a finding)
607
+ const findings = [];
608
+ if (/\.env(?:\.|$)/.test(filePath)) {
609
+ findings.push({
610
+ id: randomUUID(),
611
+ severity: "medium",
612
+ category: "secret",
613
+ rule: "env-file-committed",
614
+ file: filePath,
615
+ line: 1,
616
+ message: ".env file detected — should not be committed to version control",
617
+ evidence: filePath,
618
+ suggestion: "Add .env files to .gitignore. Use .env.example for templates.",
619
+ confidence: 1.0,
620
+ });
621
+ }
622
+ return [
623
+ ...findings,
624
+ ...this.scanWithPatterns(content, filePath, SECRET_PATTERNS),
625
+ ];
626
+ }
627
+ // ────────────────────────────────────────────────────────────────────
628
+ // Individual scans — Injection
629
+ // ────────────────────────────────────────────────────────────────────
630
+ /**
631
+ * Scan file contents for injection vulnerabilities (SQL, command, eval).
632
+ *
633
+ * @param content File content to scan
634
+ * @param filePath Relative file path for reporting
635
+ * @returns Array of findings
636
+ */
637
+ scanFileForInjection(content, filePath) {
638
+ return this.scanWithPatterns(content, filePath, INJECTION_PATTERNS);
639
+ }
640
+ // ────────────────────────────────────────────────────────────────────
641
+ // Individual scans — XSS
642
+ // ────────────────────────────────────────────────────────────────────
643
+ /**
644
+ * Scan file contents for XSS vulnerabilities.
645
+ *
646
+ * @param content File content to scan
647
+ * @param filePath Relative file path for reporting
648
+ * @returns Array of findings
649
+ */
650
+ scanFileForXSS(content, filePath) {
651
+ return this.scanWithPatterns(content, filePath, XSS_PATTERNS);
652
+ }
653
+ // ────────────────────────────────────────────────────────────────────
654
+ // Individual scans — Dependencies
655
+ // ────────────────────────────────────────────────────────────────────
656
+ /**
657
+ * Scan package.json content for known vulnerable and deprecated dependencies.
658
+ *
659
+ * @param packageJsonContent Raw package.json content string
660
+ * @returns Array of findings
661
+ */
662
+ scanDependencies(packageJsonContent) {
663
+ const findings = [];
664
+ let pkg;
665
+ try {
666
+ pkg = JSON.parse(packageJsonContent);
667
+ }
668
+ catch {
669
+ return findings;
670
+ }
671
+ const allDeps = {
672
+ ...pkg.dependencies,
673
+ ...pkg.devDependencies,
674
+ };
675
+ // Check vulnerable packages
676
+ for (const vuln of VULNERABLE_PACKAGES) {
677
+ if (allDeps[vuln.name]) {
678
+ const version = allDeps[vuln.name].replace(/^[\^~>=<]*/g, "");
679
+ if (this.isVersionVulnerable(version, vuln.vulnerableVersions)) {
680
+ findings.push({
681
+ id: randomUUID(),
682
+ severity: vuln.severity,
683
+ category: "dependency",
684
+ rule: `vuln-${vuln.name}`,
685
+ file: "package.json",
686
+ line: this.findDepLine(packageJsonContent, vuln.name),
687
+ message: `${vuln.message}${vuln.cve ? ` (${vuln.cve})` : ""}`,
688
+ evidence: `${vuln.name}@${allDeps[vuln.name]}`,
689
+ suggestion: vuln.suggestion,
690
+ confidence: 0.9,
691
+ });
692
+ }
693
+ }
694
+ }
695
+ // Check deprecated packages
696
+ for (const dep of DEPRECATED_PACKAGES) {
697
+ if (allDeps[dep.name]) {
698
+ findings.push({
699
+ id: randomUUID(),
700
+ severity: dep.severity,
701
+ category: "dependency",
702
+ rule: `deprecated-${dep.name}`,
703
+ file: "package.json",
704
+ line: this.findDepLine(packageJsonContent, dep.name),
705
+ message: dep.message,
706
+ evidence: `${dep.name}@${allDeps[dep.name]}`,
707
+ suggestion: dep.suggestion,
708
+ confidence: 0.8,
709
+ });
710
+ }
711
+ }
712
+ return findings;
713
+ }
714
+ // ────────────────────────────────────────────────────────────────────
715
+ // Individual scans — Config
716
+ // ────────────────────────────────────────────────────────────────────
717
+ /**
718
+ * Scan configuration files for security issues.
719
+ *
720
+ * @param files Map of relative file paths to their contents
721
+ * @returns Array of findings
722
+ */
723
+ scanConfig(files) {
724
+ const findings = [];
725
+ for (const [filePath, content] of files) {
726
+ // tsconfig strict mode check
727
+ if (filePath.endsWith("tsconfig.json") || filePath.endsWith("tsconfig.base.json")) {
728
+ findings.push(...this.scanTsConfig(content, filePath));
729
+ }
730
+ // Check for permissive CORS
731
+ if (this.isCodeFile(filePath)) {
732
+ findings.push(...this.scanForCorsIssues(content, filePath));
733
+ findings.push(...this.scanForMissingSecurityHeaders(content, filePath));
734
+ findings.push(...this.scanForUnsafeCSP(content, filePath));
735
+ }
736
+ }
737
+ return findings;
738
+ }
739
+ // ────────────────────────────────────────────────────────────────────
740
+ // Diff-based scan
741
+ // ────────────────────────────────────────────────────────────────────
742
+ /**
743
+ * Scan git diff output for security issues in newly added lines.
744
+ * Only scans lines that start with '+' (additions).
745
+ *
746
+ * @param diffContent Raw git diff output
747
+ * @returns Array of findings for newly added code
748
+ */
749
+ scanDiff(diffContent) {
750
+ const findings = [];
751
+ const lines = diffContent.split("\n");
752
+ let currentFile = "";
753
+ let currentLine = 0;
754
+ for (const line of lines) {
755
+ // Track current file from diff headers
756
+ const fileMatch = /^\+\+\+ b\/(.+)$/.exec(line);
757
+ if (fileMatch) {
758
+ currentFile = fileMatch[1];
759
+ continue;
760
+ }
761
+ // Track line numbers from hunk headers
762
+ const hunkMatch = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
763
+ if (hunkMatch) {
764
+ currentLine = parseInt(hunkMatch[1], 10);
765
+ continue;
766
+ }
767
+ // Only scan added lines
768
+ if (line.startsWith("+") && !line.startsWith("+++")) {
769
+ const addedContent = line.slice(1);
770
+ const allPatterns = [
771
+ ...SECRET_PATTERNS,
772
+ ...INJECTION_PATTERNS,
773
+ ...XSS_PATTERNS,
774
+ ...TRAVERSAL_PATTERNS,
775
+ ...CRYPTO_PATTERNS,
776
+ ...SSRF_PATTERNS,
777
+ ...POLLUTION_PATTERNS,
778
+ ...this.config.customPatterns,
779
+ ];
780
+ for (const pattern of allPatterns) {
781
+ if (this.config.ignoreRules.includes(pattern.name))
782
+ continue;
783
+ const match = pattern.regex.exec(addedContent);
784
+ if (match) {
785
+ findings.push({
786
+ id: randomUUID(),
787
+ severity: pattern.severity,
788
+ category: pattern.category,
789
+ rule: pattern.name,
790
+ file: currentFile,
791
+ line: currentLine,
792
+ column: match.index + 1,
793
+ message: pattern.message,
794
+ evidence: this.maskSecret(match[0]),
795
+ suggestion: pattern.suggestion,
796
+ confidence: this.patternConfidence(pattern),
797
+ });
798
+ }
799
+ }
800
+ currentLine++;
801
+ }
802
+ else if (!line.startsWith("-")) {
803
+ // Context line (no prefix) — increment line counter
804
+ currentLine++;
805
+ }
806
+ }
807
+ return findings;
808
+ }
809
+ // ────────────────────────────────────────────────────────────────────
810
+ // Utility methods
811
+ // ────────────────────────────────────────────────────────────────────
812
+ /**
813
+ * Mask a secret value for safe display.
814
+ * Shows at most the first 4 and last 2 characters.
815
+ *
816
+ * @param value The secret value to mask
817
+ * @returns Masked string (e.g., "AKIA****xy")
818
+ */
819
+ maskSecret(value) {
820
+ if (value.length <= 6)
821
+ return "****";
822
+ if (value.length <= 10) {
823
+ return value.slice(0, 2) + "****" + value.slice(-2);
824
+ }
825
+ return value.slice(0, 4) + "****" + value.slice(-2);
826
+ }
827
+ /**
828
+ * Check whether a file path should be ignored based on config.
829
+ *
830
+ * @param filePath File path to check (relative or absolute)
831
+ * @returns true if the path should be ignored
832
+ */
833
+ isIgnored(filePath) {
834
+ const relPath = filePath.startsWith("/")
835
+ ? relative(this.config.projectPath, filePath)
836
+ : filePath;
837
+ const allIgnore = [...DEFAULT_IGNORE_PATHS, ...this.config.ignorePaths];
838
+ for (const pattern of allIgnore) {
839
+ if (relPath.includes(pattern))
840
+ return true;
841
+ }
842
+ return false;
843
+ }
844
+ // ────────────────────────────────────────────────────────────────────
845
+ // Private helpers
846
+ // ────────────────────────────────────────────────────────────────────
847
+ /**
848
+ * Scan content with a set of patterns and return findings.
849
+ */
850
+ scanWithPatterns(content, filePath, patterns) {
851
+ const findings = [];
852
+ const lines = content.split("\n");
853
+ for (let i = 0; i < lines.length; i++) {
854
+ const line = lines[i];
855
+ for (const pattern of patterns) {
856
+ if (this.config.ignoreRules.includes(pattern.name))
857
+ continue;
858
+ // Reset regex lastIndex for safety
859
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
860
+ const match = regex.exec(line);
861
+ if (match) {
862
+ findings.push({
863
+ id: randomUUID(),
864
+ severity: pattern.severity,
865
+ category: pattern.category,
866
+ rule: pattern.name,
867
+ file: filePath,
868
+ line: i + 1,
869
+ column: match.index + 1,
870
+ message: pattern.message,
871
+ evidence: this.maskSecret(match[0]),
872
+ suggestion: pattern.suggestion,
873
+ confidence: this.patternConfidence(pattern),
874
+ });
875
+ }
876
+ }
877
+ }
878
+ return findings;
879
+ }
880
+ /** Scan for path traversal patterns */
881
+ scanFileForTraversal(content, filePath) {
882
+ return this.scanWithPatterns(content, filePath, TRAVERSAL_PATTERNS);
883
+ }
884
+ /** Scan for insecure crypto patterns */
885
+ scanFileForCrypto(content, filePath) {
886
+ return this.scanWithPatterns(content, filePath, CRYPTO_PATTERNS);
887
+ }
888
+ /** Scan for SSRF patterns */
889
+ scanFileForSSRF(content, filePath) {
890
+ return this.scanWithPatterns(content, filePath, SSRF_PATTERNS);
891
+ }
892
+ /** Scan for prototype pollution patterns */
893
+ scanFileForPrototypePollution(content, filePath) {
894
+ return this.scanWithPatterns(content, filePath, POLLUTION_PATTERNS);
895
+ }
896
+ /** Scan with custom patterns */
897
+ scanFileWithCustomPatterns(content, filePath) {
898
+ if (this.config.customPatterns.length === 0)
899
+ return [];
900
+ return this.scanWithPatterns(content, filePath, this.config.customPatterns);
901
+ }
902
+ /** Check tsconfig for strict mode */
903
+ scanTsConfig(content, filePath) {
904
+ const findings = [];
905
+ try {
906
+ const config = JSON.parse(content);
907
+ if (config.compilerOptions && config.compilerOptions.strict !== true) {
908
+ // Check if it's not extending a base that has strict
909
+ if (!content.includes('"extends"')) {
910
+ findings.push({
911
+ id: randomUUID(),
912
+ severity: "medium",
913
+ category: "config",
914
+ rule: "tsconfig-no-strict",
915
+ file: filePath,
916
+ line: 1,
917
+ message: "TypeScript strict mode is not enabled",
918
+ evidence: '"strict": false or missing',
919
+ suggestion: 'Enable "strict": true in compilerOptions for stronger type safety.',
920
+ confidence: 0.8,
921
+ });
922
+ }
923
+ }
924
+ }
925
+ catch {
926
+ // Skip unparseable tsconfig
927
+ }
928
+ return findings;
929
+ }
930
+ /** Check for permissive CORS configuration */
931
+ scanForCorsIssues(content, filePath) {
932
+ const findings = [];
933
+ const lines = content.split("\n");
934
+ for (let i = 0; i < lines.length; i++) {
935
+ const line = lines[i];
936
+ // origin: "*" or origin: '*'
937
+ if (/origin\s*:\s*["']\*["']/.test(line)) {
938
+ findings.push({
939
+ id: randomUUID(),
940
+ severity: "medium",
941
+ category: "config",
942
+ rule: "cors-wildcard-origin",
943
+ file: filePath,
944
+ line: i + 1,
945
+ message: "Wildcard CORS origin detected — allows any domain",
946
+ evidence: 'origin: "*"',
947
+ suggestion: "Restrict CORS to specific allowed origins. Use a whitelist in production.",
948
+ confidence: 0.9,
949
+ });
950
+ }
951
+ // Access-Control-Allow-Origin: *
952
+ if (/Access-Control-Allow-Origin["']\s*,\s*["']\*["']/.test(line) ||
953
+ /["']Access-Control-Allow-Origin["'].*["']\*["']/.test(line)) {
954
+ findings.push({
955
+ id: randomUUID(),
956
+ severity: "medium",
957
+ category: "config",
958
+ rule: "cors-header-wildcard",
959
+ file: filePath,
960
+ line: i + 1,
961
+ message: "Wildcard Access-Control-Allow-Origin header",
962
+ evidence: "Access-Control-Allow-Origin: *",
963
+ suggestion: "Restrict to specific origins in production.",
964
+ confidence: 0.9,
965
+ });
966
+ }
967
+ }
968
+ return findings;
969
+ }
970
+ /** Check for missing security headers */
971
+ scanForMissingSecurityHeaders(content, filePath) {
972
+ const findings = [];
973
+ // Only check files that look like server configuration
974
+ if (!/(express|helmet|next\.config|server\.(ts|js)|app\.(ts|js)|middleware\.(ts|js))/.test(filePath)) {
975
+ return findings;
976
+ }
977
+ // Check if it's an Express/server file without helmet
978
+ if (/\bexpress\s*\(/.test(content) && !/helmet/.test(content)) {
979
+ findings.push({
980
+ id: randomUUID(),
981
+ severity: "medium",
982
+ category: "config",
983
+ rule: "missing-helmet",
984
+ file: filePath,
985
+ line: 1,
986
+ message: "Express app without helmet — missing security headers",
987
+ evidence: "express() without helmet()",
988
+ suggestion: "Install and use 'helmet' middleware for secure HTTP headers.",
989
+ confidence: 0.7,
990
+ });
991
+ }
992
+ return findings;
993
+ }
994
+ /** Check for unsafe CSP directives */
995
+ scanForUnsafeCSP(content, filePath) {
996
+ const findings = [];
997
+ const lines = content.split("\n");
998
+ for (let i = 0; i < lines.length; i++) {
999
+ const line = lines[i];
1000
+ if (/['"]unsafe-inline['"]/.test(line) && /script-src/.test(line)) {
1001
+ findings.push({
1002
+ id: randomUUID(),
1003
+ severity: "medium",
1004
+ category: "config",
1005
+ rule: "csp-unsafe-inline",
1006
+ file: filePath,
1007
+ line: i + 1,
1008
+ message: "CSP allows 'unsafe-inline' for scripts — weakens XSS protection",
1009
+ evidence: "script-src 'unsafe-inline'",
1010
+ suggestion: "Use nonces or hashes instead of 'unsafe-inline' in script-src.",
1011
+ confidence: 0.8,
1012
+ });
1013
+ }
1014
+ if (/['"]unsafe-eval['"]/.test(line) && /script-src/.test(line)) {
1015
+ findings.push({
1016
+ id: randomUUID(),
1017
+ severity: "high",
1018
+ category: "config",
1019
+ rule: "csp-unsafe-eval",
1020
+ file: filePath,
1021
+ line: i + 1,
1022
+ message: "CSP allows 'unsafe-eval' for scripts — enables eval() attacks",
1023
+ evidence: "script-src 'unsafe-eval'",
1024
+ suggestion: "Remove 'unsafe-eval' from CSP. Refactor code to avoid eval().",
1025
+ confidence: 0.9,
1026
+ });
1027
+ }
1028
+ }
1029
+ return findings;
1030
+ }
1031
+ /** Recursively discover files to scan */
1032
+ async discoverFiles(dir, basePath) {
1033
+ const root = basePath ?? dir;
1034
+ const files = [];
1035
+ try {
1036
+ const entries = await readdir(dir);
1037
+ for (const entry of entries) {
1038
+ const fullPath = join(dir, entry);
1039
+ const relPath = relative(root, fullPath);
1040
+ if (this.isIgnored(relPath))
1041
+ continue;
1042
+ try {
1043
+ const st = await stat(fullPath);
1044
+ if (st.isDirectory()) {
1045
+ files.push(...(await this.discoverFiles(fullPath, root)));
1046
+ }
1047
+ else if (st.isFile()) {
1048
+ const ext = extname(entry).toLowerCase();
1049
+ if (SCANNABLE_EXTENSIONS.has(ext) || entry === "package.json" || entry.startsWith(".env")) {
1050
+ files.push(relPath);
1051
+ }
1052
+ }
1053
+ }
1054
+ catch {
1055
+ // Skip inaccessible entries
1056
+ }
1057
+ }
1058
+ }
1059
+ catch {
1060
+ // Skip unreadable directories
1061
+ }
1062
+ return files;
1063
+ }
1064
+ /** Simple semver-like check: is the given version within a vulnerable range */
1065
+ isVersionVulnerable(version, vulnerableRange) {
1066
+ // Parse "<X.Y.Z" pattern
1067
+ const ltMatch = /^<(\d+)\.(\d+)\.(\d+)$/.exec(vulnerableRange);
1068
+ if (!ltMatch)
1069
+ return false;
1070
+ const parts = version.split(".").map((p) => parseInt(p, 10));
1071
+ const vulnParts = [parseInt(ltMatch[1], 10), parseInt(ltMatch[2], 10), parseInt(ltMatch[3], 10)];
1072
+ // version < vulnVersion
1073
+ if (isNaN(parts[0]) || isNaN(parts[1]) || isNaN(parts[2]))
1074
+ return false;
1075
+ if (parts[0] < vulnParts[0])
1076
+ return true;
1077
+ if (parts[0] > vulnParts[0])
1078
+ return false;
1079
+ if (parts[1] < vulnParts[1])
1080
+ return true;
1081
+ if (parts[1] > vulnParts[1])
1082
+ return false;
1083
+ return parts[2] < vulnParts[2];
1084
+ }
1085
+ /** Find the line number of a dependency in package.json */
1086
+ findDepLine(content, depName) {
1087
+ const lines = content.split("\n");
1088
+ for (let i = 0; i < lines.length; i++) {
1089
+ if (lines[i].includes(`"${depName}"`))
1090
+ return i + 1;
1091
+ }
1092
+ return 1;
1093
+ }
1094
+ /** Check if a file is a code file worth scanning */
1095
+ isCodeFile(filePath) {
1096
+ const ext = extname(filePath).toLowerCase();
1097
+ return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
1098
+ }
1099
+ /** Get confidence score based on pattern category */
1100
+ patternConfidence(pattern) {
1101
+ switch (pattern.category) {
1102
+ case "secret":
1103
+ // Specific token patterns get higher confidence
1104
+ if (pattern.name.includes("aws-access") || pattern.name.includes("github-token") ||
1105
+ pattern.name.includes("stripe") || pattern.name.includes("slack") ||
1106
+ pattern.name.includes("private-key")) {
1107
+ return 0.95;
1108
+ }
1109
+ return 0.7;
1110
+ case "injection":
1111
+ return pattern.name.includes("eval") ? 0.9 : 0.75;
1112
+ case "xss":
1113
+ return 0.8;
1114
+ case "dependency":
1115
+ return 0.9;
1116
+ case "config":
1117
+ return 0.8;
1118
+ case "crypto":
1119
+ return pattern.name === "weak-random" ? 0.5 : 0.85;
1120
+ case "traversal":
1121
+ return 0.7;
1122
+ case "ssrf":
1123
+ return 0.7;
1124
+ default:
1125
+ return 0.6;
1126
+ }
1127
+ }
1128
+ }
1129
+ //# sourceMappingURL=security-scanner.js.map