@datahogo/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 (153) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +17 -0
  3. package/dist/engines/config.d.ts +3 -0
  4. package/dist/engines/config.d.ts.map +1 -0
  5. package/dist/engines/config.js +433 -0
  6. package/dist/engines/config.js.map +1 -0
  7. package/dist/engines/dast.d.ts +3 -0
  8. package/dist/engines/dast.d.ts.map +1 -0
  9. package/dist/engines/dast.js +167 -0
  10. package/dist/engines/dast.js.map +1 -0
  11. package/dist/engines/db-rules.d.ts +3 -0
  12. package/dist/engines/db-rules.d.ts.map +1 -0
  13. package/dist/engines/db-rules.js +166 -0
  14. package/dist/engines/db-rules.js.map +1 -0
  15. package/dist/engines/dependencies.d.ts +3 -0
  16. package/dist/engines/dependencies.d.ts.map +1 -0
  17. package/dist/engines/dependencies.js +167 -0
  18. package/dist/engines/dependencies.js.map +1 -0
  19. package/dist/engines/github-actions.d.ts +3 -0
  20. package/dist/engines/github-actions.d.ts.map +1 -0
  21. package/dist/engines/github-actions.js +215 -0
  22. package/dist/engines/github-actions.js.map +1 -0
  23. package/dist/engines/gitleaks.d.ts +3 -0
  24. package/dist/engines/gitleaks.d.ts.map +1 -0
  25. package/dist/engines/gitleaks.js +107 -0
  26. package/dist/engines/gitleaks.js.map +1 -0
  27. package/dist/engines/npm-audit.d.ts +3 -0
  28. package/dist/engines/npm-audit.d.ts.map +1 -0
  29. package/dist/engines/npm-audit.js +113 -0
  30. package/dist/engines/npm-audit.js.map +1 -0
  31. package/dist/engines/patterns.d.ts +3 -0
  32. package/dist/engines/patterns.d.ts.map +1 -0
  33. package/dist/engines/patterns.js +1330 -0
  34. package/dist/engines/patterns.js.map +1 -0
  35. package/dist/engines/secrets.d.ts +4 -0
  36. package/dist/engines/secrets.d.ts.map +1 -0
  37. package/dist/engines/secrets.js +260 -0
  38. package/dist/engines/secrets.js.map +1 -0
  39. package/dist/engines/semgrep.d.ts +3 -0
  40. package/dist/engines/semgrep.d.ts.map +1 -0
  41. package/dist/engines/semgrep.js +173 -0
  42. package/dist/engines/semgrep.js.map +1 -0
  43. package/dist/engines/types.d.ts +30 -0
  44. package/dist/engines/types.d.ts.map +1 -0
  45. package/dist/engines/types.js +3 -0
  46. package/dist/engines/types.js.map +1 -0
  47. package/dist/engines/url-scanner.d.ts +3 -0
  48. package/dist/engines/url-scanner.d.ts.map +1 -0
  49. package/dist/engines/url-scanner.js +469 -0
  50. package/dist/engines/url-scanner.js.map +1 -0
  51. package/dist/index.d.ts +8 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +8 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/orchestrator.d.ts +32 -0
  56. package/dist/orchestrator.d.ts.map +1 -0
  57. package/dist/orchestrator.js +237 -0
  58. package/dist/orchestrator.js.map +1 -0
  59. package/dist/rules/.gitkeep +0 -0
  60. package/dist/rules/ai-llm-security.yaml +199 -0
  61. package/dist/rules/api-security.yaml +489 -0
  62. package/dist/rules/auth-patterns.yaml +406 -0
  63. package/dist/rules/crypto-patterns.yaml +227 -0
  64. package/dist/rules/database-cloud-security.yaml +169 -0
  65. package/dist/rules/general-security.yaml +588 -0
  66. package/dist/rules/injection-patterns.yaml +318 -0
  67. package/dist/rules/nextjs-security.yaml +532 -0
  68. package/dist/rules/node-security.yaml +380 -0
  69. package/dist/rules/rules/.gitkeep +0 -0
  70. package/dist/rules/rules/ai-llm-security.yaml +199 -0
  71. package/dist/rules/rules/api-security.yaml +489 -0
  72. package/dist/rules/rules/auth-patterns.yaml +406 -0
  73. package/dist/rules/rules/crypto-patterns.yaml +227 -0
  74. package/dist/rules/rules/database-cloud-security.yaml +169 -0
  75. package/dist/rules/rules/general-security.yaml +588 -0
  76. package/dist/rules/rules/injection-patterns.yaml +318 -0
  77. package/dist/rules/rules/nextjs-security.yaml +532 -0
  78. package/dist/rules/rules/node-security.yaml +380 -0
  79. package/dist/rules/rules/supabase-security.yaml +387 -0
  80. package/dist/rules/supabase-security.yaml +387 -0
  81. package/dist/scanning/agents/csharp-agent.d.ts +12 -0
  82. package/dist/scanning/agents/csharp-agent.d.ts.map +1 -0
  83. package/dist/scanning/agents/csharp-agent.js +592 -0
  84. package/dist/scanning/agents/csharp-agent.js.map +1 -0
  85. package/dist/scanning/agents/go-agent.d.ts +14 -0
  86. package/dist/scanning/agents/go-agent.d.ts.map +1 -0
  87. package/dist/scanning/agents/go-agent.js +641 -0
  88. package/dist/scanning/agents/go-agent.js.map +1 -0
  89. package/dist/scanning/agents/java-agent.d.ts +8 -0
  90. package/dist/scanning/agents/java-agent.d.ts.map +1 -0
  91. package/dist/scanning/agents/java-agent.js +807 -0
  92. package/dist/scanning/agents/java-agent.js.map +1 -0
  93. package/dist/scanning/agents/javascript-agent.d.ts +8 -0
  94. package/dist/scanning/agents/javascript-agent.d.ts.map +1 -0
  95. package/dist/scanning/agents/javascript-agent.js +77 -0
  96. package/dist/scanning/agents/javascript-agent.js.map +1 -0
  97. package/dist/scanning/agents/mobile-agent.d.ts +8 -0
  98. package/dist/scanning/agents/mobile-agent.d.ts.map +1 -0
  99. package/dist/scanning/agents/mobile-agent.js +916 -0
  100. package/dist/scanning/agents/mobile-agent.js.map +1 -0
  101. package/dist/scanning/agents/php-agent.d.ts +8 -0
  102. package/dist/scanning/agents/php-agent.d.ts.map +1 -0
  103. package/dist/scanning/agents/php-agent.js +679 -0
  104. package/dist/scanning/agents/php-agent.js.map +1 -0
  105. package/dist/scanning/agents/python-agent.d.ts +8 -0
  106. package/dist/scanning/agents/python-agent.d.ts.map +1 -0
  107. package/dist/scanning/agents/python-agent.js +701 -0
  108. package/dist/scanning/agents/python-agent.js.map +1 -0
  109. package/dist/scanning/agents/supabase-agent.d.ts +8 -0
  110. package/dist/scanning/agents/supabase-agent.d.ts.map +1 -0
  111. package/dist/scanning/agents/supabase-agent.js +484 -0
  112. package/dist/scanning/agents/supabase-agent.js.map +1 -0
  113. package/dist/scanning/orchestrator.d.ts +29 -0
  114. package/dist/scanning/orchestrator.d.ts.map +1 -0
  115. package/dist/scanning/orchestrator.js +186 -0
  116. package/dist/scanning/orchestrator.js.map +1 -0
  117. package/dist/scanning/tech-detector.d.ts +12 -0
  118. package/dist/scanning/tech-detector.d.ts.map +1 -0
  119. package/dist/scanning/tech-detector.js +252 -0
  120. package/dist/scanning/tech-detector.js.map +1 -0
  121. package/dist/scanning/types.d.ts +87 -0
  122. package/dist/scanning/types.d.ts.map +1 -0
  123. package/dist/scanning/types.js +5 -0
  124. package/dist/scanning/types.js.map +1 -0
  125. package/dist/utils/child-process.d.ts +2 -0
  126. package/dist/utils/child-process.d.ts.map +1 -0
  127. package/dist/utils/child-process.js +4 -0
  128. package/dist/utils/child-process.js.map +1 -0
  129. package/dist/utils/context-classifier.d.ts +11 -0
  130. package/dist/utils/context-classifier.d.ts.map +1 -0
  131. package/dist/utils/context-classifier.js +79 -0
  132. package/dist/utils/context-classifier.js.map +1 -0
  133. package/dist/utils/exec.d.ts +18 -0
  134. package/dist/utils/exec.d.ts.map +1 -0
  135. package/dist/utils/exec.js +51 -0
  136. package/dist/utils/exec.js.map +1 -0
  137. package/dist/utils/file-filter.d.ts +4 -0
  138. package/dist/utils/file-filter.d.ts.map +1 -0
  139. package/dist/utils/file-filter.js +68 -0
  140. package/dist/utils/file-filter.js.map +1 -0
  141. package/dist/utils/fs.d.ts +2 -0
  142. package/dist/utils/fs.d.ts.map +1 -0
  143. package/dist/utils/fs.js +5 -0
  144. package/dist/utils/fs.js.map +1 -0
  145. package/dist/utils/logger.d.ts +12 -0
  146. package/dist/utils/logger.d.ts.map +1 -0
  147. package/dist/utils/logger.js +27 -0
  148. package/dist/utils/logger.js.map +1 -0
  149. package/dist/utils/post-processor.d.ts +10 -0
  150. package/dist/utils/post-processor.d.ts.map +1 -0
  151. package/dist/utils/post-processor.js +183 -0
  152. package/dist/utils/post-processor.js.map +1 -0
  153. package/package.json +44 -0
@@ -0,0 +1,679 @@
1
+ // PHP / Laravel ecosystem security scanner agent.
2
+ // Detects PHP projects via composer.json and scans for
3
+ // mass assignment, SQL injection, debug mode, dangerous functions,
4
+ // unsafe deserialization, XSS, missing auth middleware, SSRF,
5
+ // weak hashing, committed .env files, CORS wildcards, and missing CSRF.
6
+ export class PHPScanAgent {
7
+ async detect(files) {
8
+ for (const filePath of files.keys()) {
9
+ if (filePath === "composer.json" || filePath.endsWith("/composer.json")) {
10
+ return true;
11
+ }
12
+ }
13
+ return false;
14
+ }
15
+ async scan(files) {
16
+ const results = [];
17
+ // Detect project-level global middleware once so individual checks can
18
+ // suppress absence-based findings that are already covered globally.
19
+ const hasGlobalAuth = detectGlobalAuthMiddleware(files);
20
+ results.push(...checkMassAssignment(files));
21
+ results.push(...checkSqlInjectionDbRaw(files));
22
+ results.push(...checkAppDebug(files));
23
+ results.push(...checkDangerousFunctions(files));
24
+ results.push(...checkUnsafeUnserialize(files));
25
+ results.push(...checkXssEcho(files));
26
+ results.push(...checkRoutesWithoutAuth(files, hasGlobalAuth));
27
+ results.push(...checkRawUserInputSql(files));
28
+ results.push(...checkSsrf(files));
29
+ results.push(...checkWeakPasswordHashing(files));
30
+ results.push(...checkCommittedEnv(files));
31
+ results.push(...checkCorsWildcard(files));
32
+ results.push(...checkMissingCsrf(files, hasGlobalAuth));
33
+ return results;
34
+ }
35
+ getMetadata() {
36
+ return {
37
+ name: "php-agent",
38
+ version: "1.0.0",
39
+ technologies: ["php", "laravel"],
40
+ };
41
+ }
42
+ getChecks() {
43
+ return [
44
+ { id: "php:mass-assignment", name: "Mass assignment without $fillable/$guarded", severity: "HIGH" },
45
+ { id: "php:sql-injection-db-raw", name: "SQL injection via DB::raw() with variable", severity: "CRITICAL" },
46
+ { id: "php:app-debug-true", name: "APP_DEBUG=true exposes stack traces", severity: "HIGH" },
47
+ { id: "php:dangerous-functions", name: "Dangerous function call (eval/exec/system/…)", severity: "CRITICAL" },
48
+ { id: "php:unsafe-unserialize", name: "Unsafe unserialize() with variable input", severity: "CRITICAL" },
49
+ { id: "php:xss-echo", name: "XSS via echo or unescaped Blade output", severity: "HIGH" },
50
+ { id: "php:routes-without-auth", name: "Modifying route without auth middleware", severity: "MEDIUM" },
51
+ { id: "php:sql-injection-raw-input", name: "SQL injection via raw $_GET/$_POST in query()", severity: "CRITICAL" },
52
+ { id: "php:ssrf-file-get-contents", name: "SSRF via file_get_contents() with variable URL", severity: "HIGH" },
53
+ { id: "php:weak-password-hashing", name: "Weak hashing algorithm: md5() or sha1()", severity: "HIGH" },
54
+ { id: "php:committed-env-secrets", name: "Secret credentials committed in .env file", severity: "CRITICAL" },
55
+ { id: "php:cors-wildcard", name: "CORS configured with wildcard origin (*)", severity: "MEDIUM" },
56
+ { id: "php:missing-csrf", name: "Missing CSRF protection in Blade form", severity: "MEDIUM" },
57
+ ];
58
+ }
59
+ }
60
+ // --- File type helpers ---
61
+ function isPHPFile(filePath) {
62
+ return filePath.endsWith(".php");
63
+ }
64
+ function isBladeFile(filePath) {
65
+ return filePath.endsWith(".blade.php");
66
+ }
67
+ function isEnvFile(filePath) {
68
+ // Matches .env, .env.local, .env.production, etc.
69
+ // Does NOT match .env.example or .env.template
70
+ if (/\.env\.example$/.test(filePath))
71
+ return false;
72
+ if (/\.env\.template$/.test(filePath))
73
+ return false;
74
+ return /(?:^|\/)\.(env)(\.\w+)?$/.test(filePath);
75
+ }
76
+ function isArtisanCommand(filePath) {
77
+ return filePath.includes("app/Console/Commands/");
78
+ }
79
+ // --- Project-level global auth middleware detection ---
80
+ // Scans Kernel.php or bootstrap/app.php for framework-level auth middleware.
81
+ // If present, absence-based findings (missing auth on routes, missing CSRF)
82
+ // should not be raised — the middleware applies globally and we can't infer
83
+ // route-level coverage from static analysis alone.
84
+ function detectGlobalAuthMiddleware(files) {
85
+ for (const [filePath, content] of files) {
86
+ if (!filePath.includes("app/Http/Kernel.php") &&
87
+ !filePath.includes("bootstrap/app.php"))
88
+ continue;
89
+ // Illuminate auth middleware or throttle/RateLimiter signals a mature setup
90
+ if (/\\Illuminate\\Auth\\Middleware\\Authenticate/.test(content))
91
+ return true;
92
+ if (/['"]auth['"]/.test(content) && /\$middlewareAliases\s*=/.test(content))
93
+ return true;
94
+ if (/throttle:/.test(content))
95
+ return true;
96
+ if (/RateLimiter/.test(content))
97
+ return true;
98
+ // bootstrap/app.php with global middleware definition (Laravel 11+)
99
+ if (/->withMiddleware\s*\(/.test(content) && /auth/.test(content))
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ // --- Check 1: Mass assignment without $fillable/$guarded ---
105
+ function checkMassAssignment(files) {
106
+ const results = [];
107
+ for (const [filePath, content] of files) {
108
+ if (!isPHPFile(filePath) || isBladeFile(filePath))
109
+ continue;
110
+ if (!/extends\s+Model/.test(content))
111
+ continue;
112
+ // Only count property declarations (protected/public/private $fillable),
113
+ // not comments that mention $fillable in documentation text.
114
+ if (/(?:protected|public|private)\s+\$fillable\b/.test(content))
115
+ continue;
116
+ if (/(?:protected|public|private)\s+\$guarded\b/.test(content))
117
+ continue;
118
+ // Report at the line where the class extends Model
119
+ const lines = content.split("\n");
120
+ let flagLine = 1;
121
+ for (let i = 0; i < lines.length; i++) {
122
+ if (/extends\s+Model/.test(lines[i])) {
123
+ flagLine = i + 1;
124
+ break;
125
+ }
126
+ }
127
+ results.push({
128
+ checkId: "php:mass-assignment",
129
+ title: "Mass assignment vulnerability: missing $fillable or $guarded",
130
+ severity: "HIGH",
131
+ confidence: "medium",
132
+ file: filePath,
133
+ line: flagLine,
134
+ description: "This Eloquent model extends Model but defines neither $fillable nor $guarded. " +
135
+ "Without a whitelist or blacklist, any field from a request payload can be mass-assigned, " +
136
+ "potentially allowing attackers to overwrite sensitive fields like 'is_admin' or 'role'.",
137
+ fix: "Define $fillable or $guarded in your Eloquent model to prevent mass assignment.",
138
+ cwe: "CWE-915",
139
+ });
140
+ }
141
+ return results;
142
+ }
143
+ // --- Check 2: SQL injection via DB::raw() ---
144
+ // Returns true when a DB::raw() call contains parameterized placeholders OR
145
+ // is given a second binding argument, making it safe.
146
+ // Examples:
147
+ // DB::raw("... WHERE x = ?", [$x]) → safe (has binding array)
148
+ // DB::raw("... WHERE x = $x") → unsafe (interpolated variable)
149
+ function isDbRawSafe(line, matchIndex) {
150
+ // Extract the full argument region after DB::raw( up to a reasonable window
151
+ const afterMatch = line.slice(matchIndex);
152
+ // If the expression contains a ? placeholder it is parameterized
153
+ if (/\?/.test(afterMatch))
154
+ return true;
155
+ // If there is a second argument (array binding) after the closing quote it is safe
156
+ // e.g. DB::raw("...", [$x]) or DB::select("...", [$x])
157
+ // We look for a pattern like: "...", [ or '...', [
158
+ if (/['"][^'"]*['"]\s*,\s*\[/.test(afterMatch))
159
+ return true;
160
+ return false;
161
+ }
162
+ function checkSqlInjectionDbRaw(files) {
163
+ const results = [];
164
+ for (const [filePath, content] of files) {
165
+ if (!isPHPFile(filePath))
166
+ continue;
167
+ const lines = content.split("\n");
168
+ for (let i = 0; i < lines.length; i++) {
169
+ const line = lines[i];
170
+ // DB::raw( with a variable inside, or DB::select(DB::raw( with variable
171
+ const rawMatch = /DB::raw\s*\(/.exec(line);
172
+ if (!rawMatch)
173
+ continue;
174
+ // Must have a variable ($ sign) somewhere after DB::raw(
175
+ const afterRaw = line.slice(rawMatch.index);
176
+ if (!/\$/.test(afterRaw))
177
+ continue;
178
+ // Skip if the call is safe (parameterized or has binding array)
179
+ if (isDbRawSafe(line, rawMatch.index))
180
+ continue;
181
+ results.push({
182
+ checkId: "php:sql-injection-db-raw",
183
+ title: "SQL injection via DB::raw() with variable input",
184
+ severity: "CRITICAL",
185
+ confidence: "high",
186
+ file: filePath,
187
+ line: i + 1,
188
+ description: "DB::raw() is used with a PHP variable directly interpolated into the SQL string. " +
189
+ "If that variable contains user-controlled data, an attacker can inject arbitrary SQL " +
190
+ "and read, modify, or delete data.",
191
+ fix: "Use query builder bindings: DB::select('SELECT * FROM users WHERE id = ?', [$id])",
192
+ cwe: "CWE-89",
193
+ });
194
+ }
195
+ }
196
+ return results;
197
+ }
198
+ // --- Check 3: APP_DEBUG=true in .env ---
199
+ function checkAppDebug(files) {
200
+ const results = [];
201
+ for (const [filePath, content] of files) {
202
+ if (!isEnvFile(filePath))
203
+ continue;
204
+ const lines = content.split("\n");
205
+ for (let i = 0; i < lines.length; i++) {
206
+ if (/^\s*APP_DEBUG\s*=\s*true\s*$/i.test(lines[i])) {
207
+ results.push({
208
+ checkId: "php:app-debug-true",
209
+ title: "APP_DEBUG=true exposes stack traces in production",
210
+ severity: "HIGH",
211
+ confidence: "high",
212
+ file: filePath,
213
+ line: i + 1,
214
+ description: "APP_DEBUG=true causes Laravel to display full stack traces, local variable dumps, " +
215
+ "and framework internals to any user who triggers an error. This leaks sensitive " +
216
+ "configuration details and aids attacker reconnaissance.",
217
+ fix: "Set APP_DEBUG=false in production .env file.",
218
+ cwe: "CWE-215",
219
+ });
220
+ }
221
+ }
222
+ }
223
+ return results;
224
+ }
225
+ // --- Check 4: Dangerous functions ---
226
+ // Matches function calls that are NOT inside a comment.
227
+ // We check that the line is not a comment line (// or #) before flagging.
228
+ const DANGEROUS_FUNCTIONS = ["eval", "exec", "system", "shell_exec", "passthru", "popen"];
229
+ const DANGEROUS_FUNC_REGEX = new RegExp(`\\b(${DANGEROUS_FUNCTIONS.join("|")})\\s*\\(`);
230
+ // Patterns that indicate user-controlled input is passed to eval/exec
231
+ const USER_INPUT_PATTERN = /\$_(POST|GET|REQUEST|COOKIE|SERVER)\b|\$request->|->input\s*\(|->get\s*\(/;
232
+ function checkDangerousFunctions(files) {
233
+ const results = [];
234
+ for (const [filePath, content] of files) {
235
+ if (!isPHPFile(filePath))
236
+ continue;
237
+ // exec/shell_exec in Artisan console commands are expected — lower confidence
238
+ const isConsoleCommand = isArtisanCommand(filePath);
239
+ const lines = content.split("\n");
240
+ for (let i = 0; i < lines.length; i++) {
241
+ const trimmed = lines[i].trimStart();
242
+ // Skip comment lines
243
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
244
+ continue;
245
+ // Strip inline comments before matching
246
+ const codeOnly = trimmed.replace(/\/\/.*$/, "").replace(/#.*$/, "");
247
+ const match = DANGEROUS_FUNC_REGEX.exec(codeOnly);
248
+ if (!match)
249
+ continue;
250
+ const funcName = match[1];
251
+ // Determine confidence based on context
252
+ let confidence;
253
+ if (funcName === "eval" && USER_INPUT_PATTERN.test(codeOnly)) {
254
+ // eval() with clear user-controlled input
255
+ confidence = "high";
256
+ }
257
+ else if (funcName === "eval") {
258
+ // eval() with other variables — still dangerous but less certain about user input
259
+ confidence = "medium";
260
+ }
261
+ else if (isConsoleCommand) {
262
+ // exec/shell_exec/etc. in Artisan commands — legitimate deployment tooling
263
+ confidence = "low";
264
+ }
265
+ else {
266
+ // Other dangerous function calls
267
+ confidence = "medium";
268
+ }
269
+ results.push({
270
+ checkId: "php:dangerous-functions",
271
+ title: `Dangerous function call: ${funcName}()`,
272
+ severity: "CRITICAL",
273
+ confidence,
274
+ file: filePath,
275
+ line: i + 1,
276
+ description: `${funcName}() executes arbitrary OS commands or evaluates arbitrary code. ` +
277
+ "If user input reaches this call, an attacker can achieve remote code execution (RCE).",
278
+ fix: "Avoid dangerous functions. Use safer alternatives or validate input strictly.",
279
+ cwe: "CWE-78",
280
+ });
281
+ }
282
+ }
283
+ return results;
284
+ }
285
+ // --- Check 5: Unsafe unserialize ---
286
+ function checkUnsafeUnserialize(files) {
287
+ const results = [];
288
+ for (const [filePath, content] of files) {
289
+ if (!isPHPFile(filePath))
290
+ continue;
291
+ const lines = content.split("\n");
292
+ for (let i = 0; i < lines.length; i++) {
293
+ const trimmed = lines[i].trimStart();
294
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
295
+ continue;
296
+ // unserialize( with any variable argument
297
+ if (/\bunserialize\s*\(\s*\$/.test(lines[i])) {
298
+ // If it is superglobal user input, confidence is higher
299
+ const isUserInput = /\$_(POST|GET|REQUEST|COOKIE)\[/.test(lines[i]);
300
+ results.push({
301
+ checkId: "php:unsafe-unserialize",
302
+ title: "Unsafe unserialize() call with variable input",
303
+ severity: "CRITICAL",
304
+ confidence: isUserInput ? "high" : "medium",
305
+ file: filePath,
306
+ line: i + 1,
307
+ description: "unserialize() can execute arbitrary PHP code during object reconstruction via " +
308
+ "__wakeup() or __destruct() magic methods. Passing user-controlled data to " +
309
+ "unserialize() is a common PHP object injection vulnerability.",
310
+ fix: "Use json_decode() instead. If unserialize is needed, use allowed_classes option.",
311
+ cwe: "CWE-502",
312
+ });
313
+ }
314
+ }
315
+ }
316
+ return results;
317
+ }
318
+ // --- Check 6: XSS via echo without escaping ---
319
+ // Safe output-escaping wrappers. We check the surrounding 50-character window
320
+ // (not just the identical position) to handle multi-function calls like nl2br(e($var)).
321
+ const SAFE_ECHO_WRAPPERS = [
322
+ /htmlspecialchars\s*\(/,
323
+ /\be\s*\(/, // Laravel's e() helper
324
+ /strip_tags\s*\(/,
325
+ /nl2br\s*\(\s*e\s*\(/, // nl2br(e($var))
326
+ ];
327
+ function hasSafeEscaping(line) {
328
+ return SAFE_ECHO_WRAPPERS.some((pattern) => pattern.test(line));
329
+ }
330
+ function checkXssEcho(files) {
331
+ const results = [];
332
+ for (const [filePath, content] of files) {
333
+ if (!isPHPFile(filePath))
334
+ continue;
335
+ const lines = content.split("\n");
336
+ if (isBladeFile(filePath)) {
337
+ // In Blade: flag {!! $var !!} (unescaped output)
338
+ for (let i = 0; i < lines.length; i++) {
339
+ if (/\{!!\s*\$/.test(lines[i])) {
340
+ results.push({
341
+ checkId: "php:xss-echo",
342
+ title: "XSS via unescaped Blade output: {!! $var !!}",
343
+ severity: "HIGH",
344
+ confidence: "medium",
345
+ file: filePath,
346
+ line: i + 1,
347
+ description: "{!! ... !!} outputs raw unescaped HTML in Blade templates. If the variable " +
348
+ "contains user input, an attacker can inject arbitrary HTML or JavaScript " +
349
+ "and execute code in victims' browsers.",
350
+ fix: "Use htmlspecialchars() or {{ }} in Blade templates for auto-escaping.",
351
+ cwe: "CWE-79",
352
+ });
353
+ }
354
+ }
355
+ }
356
+ else {
357
+ // In plain PHP: flag echo $var without any safe escaping wrapper
358
+ for (let i = 0; i < lines.length; i++) {
359
+ const trimmed = lines[i].trimStart();
360
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
361
+ continue;
362
+ if (!/\becho\s+\$/.test(lines[i]))
363
+ continue;
364
+ // Check if the line contains any recognised safe escaping wrapper
365
+ if (hasSafeEscaping(lines[i]))
366
+ continue;
367
+ results.push({
368
+ checkId: "php:xss-echo",
369
+ title: "XSS via echo without output escaping",
370
+ severity: "HIGH",
371
+ confidence: "medium",
372
+ file: filePath,
373
+ line: i + 1,
374
+ description: "echo is used to output a PHP variable directly without escaping. If the variable " +
375
+ "contains user-supplied data, an attacker can inject arbitrary HTML or JavaScript.",
376
+ fix: "Use htmlspecialchars() or {{ }} in Blade templates for auto-escaping.",
377
+ cwe: "CWE-79",
378
+ });
379
+ }
380
+ }
381
+ }
382
+ return results;
383
+ }
384
+ // --- Check 7: Routes without auth middleware ---
385
+ const MODIFYING_METHODS = ["post", "put", "patch", "delete"];
386
+ void MODIFYING_METHODS; // referenced indirectly via the regex below
387
+ // Paths that legitimately do not require authentication
388
+ const EXEMPT_PATHS = [
389
+ "/login",
390
+ "/register",
391
+ "/webhook",
392
+ "/callback",
393
+ "/oauth",
394
+ "/stripe",
395
+ "/health",
396
+ "/api/public",
397
+ ];
398
+ // All middleware patterns that indicate an auth guard is present
399
+ const AUTH_MIDDLEWARE_PATTERNS = [
400
+ /middleware\s*\(\s*['"]auth['"]\s*\)/, // ->middleware('auth')
401
+ /middleware\s*\(\s*['"]auth:api['"]\s*\)/, // ->middleware('auth:api')
402
+ /middleware\s*\(\s*['"]auth:sanctum['"]\s*\)/, // ->middleware('auth:sanctum')
403
+ /middleware\s*\(\s*['"]auth:web['"]\s*\)/, // ->middleware('auth:web')
404
+ /middleware\s*\(\s*\[['"]auth['"]/.source, // ->middleware(['auth', ...])
405
+ ];
406
+ // Compiled single regex for speed — any match = auth is present
407
+ const AUTH_MIDDLEWARE_REGEX = new RegExp(AUTH_MIDDLEWARE_PATTERNS.map((p) => (p instanceof RegExp ? p.source : p)).join("|"));
408
+ // File-level Route::middleware('auth')->group( wraps all routes in the file
409
+ const FILE_LEVEL_AUTH_REGEX = /Route::middleware\s*\(\s*['"]auth(?::[a-z]+)?['"]\s*\)\s*->\s*group\s*\(/;
410
+ function checkRoutesWithoutAuth(files, hasGlobalAuth) {
411
+ const results = [];
412
+ // When global auth middleware is configured project-wide, we cannot determine
413
+ // coverage from static analysis of route files alone, so suppress these findings.
414
+ if (hasGlobalAuth)
415
+ return results;
416
+ for (const [filePath, content] of files) {
417
+ if (!isPHPFile(filePath))
418
+ continue;
419
+ // Only scan route files
420
+ if (!filePath.includes("routes/web.php") && !filePath.includes("routes/api.php"))
421
+ continue;
422
+ // If the entire file is wrapped in Route::middleware('auth')->group(, skip it
423
+ if (FILE_LEVEL_AUTH_REGEX.test(content))
424
+ continue;
425
+ const lines = content.split("\n");
426
+ for (let i = 0; i < lines.length; i++) {
427
+ const line = lines[i];
428
+ // Match Route::post|put|patch|delete(
429
+ const methodMatch = /Route::(post|put|patch|delete)\s*\(/i.exec(line);
430
+ if (!methodMatch)
431
+ continue;
432
+ // Check if path is an exempt endpoint.
433
+ // We look for the exempt segment anywhere inside a quoted URL argument,
434
+ // so '/webhook', '/webhook/github', '/stripe/webhook' all match '/webhook' and '/stripe'.
435
+ const isExempt = EXEMPT_PATHS.some((p) => {
436
+ // Match the segment as a path component (after a quote or slash) to avoid
437
+ // spurious matches like '/oauthother' matching '/oauth'.
438
+ const escapedP = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
439
+ return new RegExp(`['"](?:[^'"]*)?${escapedP}(?:[^'"]*)?['"]`).test(line);
440
+ });
441
+ if (isExempt)
442
+ continue;
443
+ // Look at the same line and the next 5 lines for any auth middleware pattern
444
+ const contextLines = lines.slice(i, Math.min(i + 6, lines.length)).join("\n");
445
+ if (AUTH_MIDDLEWARE_REGEX.test(contextLines))
446
+ continue;
447
+ results.push({
448
+ checkId: "php:routes-without-auth",
449
+ title: `Route::${methodMatch[1]}() without auth middleware`,
450
+ severity: "MEDIUM",
451
+ confidence: "low",
452
+ file: filePath,
453
+ line: i + 1,
454
+ description: `A ${methodMatch[1].toUpperCase()} route is defined without an 'auth' middleware guard. ` +
455
+ "Unauthenticated users may be able to perform state-changing operations.",
456
+ fix: "Add auth middleware: Route::post('/path', ...)->middleware('auth')",
457
+ cwe: "CWE-306",
458
+ });
459
+ }
460
+ }
461
+ return results;
462
+ }
463
+ // --- Check 8: SQL injection via raw $_GET/$_POST in query ---
464
+ function checkRawUserInputSql(files) {
465
+ const results = [];
466
+ for (const [filePath, content] of files) {
467
+ if (!isPHPFile(filePath))
468
+ continue;
469
+ const lines = content.split("\n");
470
+ for (let i = 0; i < lines.length; i++) {
471
+ const trimmed = lines[i].trimStart();
472
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
473
+ continue;
474
+ // Line contains both superglobal input and a raw query call
475
+ if (/\$_(GET|POST|REQUEST)\[/.test(lines[i]) &&
476
+ /query\s*\(/.test(lines[i])) {
477
+ results.push({
478
+ checkId: "php:sql-injection-raw-input",
479
+ title: "SQL injection via raw user input in query()",
480
+ severity: "CRITICAL",
481
+ confidence: "high",
482
+ file: filePath,
483
+ line: i + 1,
484
+ description: "User-supplied data from $_GET, $_POST, or $_REQUEST is used directly inside a " +
485
+ "query() call without parameterization. An attacker can inject arbitrary SQL.",
486
+ fix: "Use prepared statements: $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');",
487
+ cwe: "CWE-89",
488
+ });
489
+ }
490
+ }
491
+ }
492
+ return results;
493
+ }
494
+ // --- Check 9: SSRF via file_get_contents / curl with variable URL ---
495
+ // Returns true if URL validation appears within `windowLines` lines before the
496
+ // file_get_contents() call.
497
+ function hasUrlValidationBefore(lines, currentIndex, windowLines = 5) {
498
+ const start = Math.max(0, currentIndex - windowLines);
499
+ const context = lines.slice(start, currentIndex + 1).join("\n");
500
+ // filter_var with FILTER_VALIDATE_URL
501
+ if (/filter_var\s*\([^,]+,\s*FILTER_VALIDATE_URL\)/.test(context))
502
+ return true;
503
+ // parse_url + domain validation pattern
504
+ if (/parse_url\s*\(/.test(context) && /host/.test(context))
505
+ return true;
506
+ return false;
507
+ }
508
+ function checkSsrf(files) {
509
+ const results = [];
510
+ for (const [filePath, content] of files) {
511
+ if (!isPHPFile(filePath))
512
+ continue;
513
+ const lines = content.split("\n");
514
+ for (let i = 0; i < lines.length; i++) {
515
+ const trimmed = lines[i].trimStart();
516
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
517
+ continue;
518
+ if (!/\bfile_get_contents\s*\(\s*\$/.test(lines[i]))
519
+ continue;
520
+ // Check whether the URL is validated within 5 lines above
521
+ const isValidated = hasUrlValidationBefore(lines, i);
522
+ const confidence = isValidated ? "low" : "high";
523
+ results.push({
524
+ checkId: "php:ssrf-file-get-contents",
525
+ title: "SSRF via file_get_contents() with variable URL",
526
+ severity: "HIGH",
527
+ confidence,
528
+ file: filePath,
529
+ line: i + 1,
530
+ description: "file_get_contents() is called with a variable URL. If that variable originates " +
531
+ "from user input, an attacker can make the server fetch arbitrary internal or " +
532
+ "external resources (SSRF — Server-Side Request Forgery).",
533
+ fix: "Validate and whitelist URLs before fetching. Use allow-lists for domains.",
534
+ cwe: "CWE-918",
535
+ });
536
+ }
537
+ }
538
+ return results;
539
+ }
540
+ // --- Check 10: Weak password hashing ---
541
+ function checkWeakPasswordHashing(files) {
542
+ const results = [];
543
+ for (const [filePath, content] of files) {
544
+ if (!isPHPFile(filePath))
545
+ continue;
546
+ const lines = content.split("\n");
547
+ for (let i = 0; i < lines.length; i++) {
548
+ const trimmed = lines[i].trimStart();
549
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
550
+ continue;
551
+ if (/\bmd5\s*\(\s*\$/.test(lines[i]) || /\bsha1\s*\(\s*\$/.test(lines[i])) {
552
+ results.push({
553
+ checkId: "php:weak-password-hashing",
554
+ title: "Weak hashing algorithm: md5() or sha1() used",
555
+ severity: "HIGH",
556
+ confidence: "medium",
557
+ file: filePath,
558
+ line: i + 1,
559
+ description: "MD5 and SHA1 are cryptographically broken algorithms with known collision attacks " +
560
+ "and fast GPU cracking speeds. They are unsuitable for password hashing and should " +
561
+ "not be used for any security-sensitive purpose.",
562
+ fix: "Use password_hash() and password_verify() for passwords.",
563
+ cwe: "CWE-328",
564
+ });
565
+ }
566
+ }
567
+ }
568
+ return results;
569
+ }
570
+ // --- Check 11: Committed .env with secrets ---
571
+ // Matches .env variable names that CONTAIN a sensitive word anywhere in the key name,
572
+ // with a non-empty value of at least 4 characters. Case-insensitive.
573
+ // Examples that match: DB_PASSWORD, STRIPE_API_SECRET, APP_KEY, MY_PRIVATE_KEY
574
+ const SECRET_KEY_PATTERN = /^[A-Z0-9_]*(?:SECRET|PASSWORD|PASSWD|TOKEN|API_KEY|PRIVATE_KEY|CREDENTIAL|APP_KEY|DB_PASSWORD)[A-Z0-9_]*\s*=\s*.{4,}$/i;
575
+ function checkCommittedEnv(files) {
576
+ const results = [];
577
+ for (const [filePath, content] of files) {
578
+ if (!isEnvFile(filePath))
579
+ continue;
580
+ const lines = content.split("\n");
581
+ for (let i = 0; i < lines.length; i++) {
582
+ const line = lines[i].trim();
583
+ if (!line || line.startsWith("#"))
584
+ continue;
585
+ if (SECRET_KEY_PATTERN.test(line)) {
586
+ results.push({
587
+ checkId: "php:committed-env-secrets",
588
+ title: "Secret credentials committed in .env file",
589
+ severity: "CRITICAL",
590
+ confidence: "high",
591
+ file: filePath,
592
+ line: i + 1,
593
+ description: "A .env file containing sensitive credentials is tracked in the repository. " +
594
+ "Anyone with read access to the repo — including collaborators and CI systems — " +
595
+ "can read these credentials. They may also persist in git history after deletion.",
596
+ fix: "Add .env to .gitignore. Rotate all exposed credentials.",
597
+ cwe: "CWE-540",
598
+ });
599
+ // One finding per .env file is sufficient
600
+ break;
601
+ }
602
+ }
603
+ }
604
+ return results;
605
+ }
606
+ // --- Check 12: CORS wildcard ---
607
+ function checkCorsWildcard(files) {
608
+ const results = [];
609
+ for (const [filePath, content] of files) {
610
+ if (!isPHPFile(filePath) && !isEnvFile(filePath))
611
+ continue;
612
+ const lines = content.split("\n");
613
+ for (let i = 0; i < lines.length; i++) {
614
+ const line = lines[i];
615
+ const trimmed = line.trimStart();
616
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
617
+ continue;
618
+ if (/CORS_ALLOWED_ORIGINS.*\*/.test(line) ||
619
+ /allowedOrigins.*\[\s*['"]?\*['"]?\s*\]/.test(line) ||
620
+ /Access-Control-Allow-Origin.*\*/.test(line)) {
621
+ results.push({
622
+ checkId: "php:cors-wildcard",
623
+ title: "CORS configured with wildcard origin (*)",
624
+ severity: "MEDIUM",
625
+ confidence: "medium",
626
+ file: filePath,
627
+ line: i + 1,
628
+ description: "CORS is configured to allow requests from any origin (*). This lets any website " +
629
+ "make cross-origin requests to your API, potentially leaking authenticated user data " +
630
+ "if credentials are included in requests.",
631
+ fix: "Restrict CORS to specific origins.",
632
+ cwe: "CWE-942",
633
+ });
634
+ }
635
+ }
636
+ }
637
+ return results;
638
+ }
639
+ // --- Check 13: Missing CSRF protection in Blade forms ---
640
+ function checkMissingCsrf(files, hasGlobalAuth) {
641
+ const results = [];
642
+ // When global middleware handles CSRF (VerifyCsrfToken is typically registered
643
+ // in Kernel.php alongside auth middleware), we can suppress these findings.
644
+ if (hasGlobalAuth)
645
+ return results;
646
+ for (const [filePath, content] of files) {
647
+ if (!isBladeFile(filePath))
648
+ continue;
649
+ const lines = content.split("\n");
650
+ for (let i = 0; i < lines.length; i++) {
651
+ // Find opening <form tags
652
+ if (!/<form\b/i.test(lines[i]))
653
+ continue;
654
+ // Look at the next 5 lines for @csrf or csrf_field()
655
+ const contextLines = lines.slice(i, Math.min(i + 6, lines.length)).join("\n");
656
+ if (/@csrf\b/.test(contextLines) || /csrf_field\s*\(\s*\)/.test(contextLines))
657
+ continue;
658
+ // Only flag forms that are likely to submit (skip forms with method="get" or no method)
659
+ // We flag by default; GET forms don't need CSRF but POST forms do
660
+ if (/method\s*=\s*['"]?get['"]?/i.test(lines[i]))
661
+ continue;
662
+ results.push({
663
+ checkId: "php:missing-csrf",
664
+ title: "Missing CSRF protection in Blade form",
665
+ severity: "MEDIUM",
666
+ confidence: "low",
667
+ file: filePath,
668
+ line: i + 1,
669
+ description: "A <form> element in a Blade template does not include @csrf or csrf_field(). " +
670
+ "Without CSRF protection, an attacker can trick authenticated users into " +
671
+ "submitting forms to your application from an external website.",
672
+ fix: 'Add @csrf inside your form: <form method="POST"> @csrf ... </form>',
673
+ cwe: "CWE-352",
674
+ });
675
+ }
676
+ }
677
+ return results;
678
+ }
679
+ //# sourceMappingURL=php-agent.js.map