laravel-security-agent 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laravel-security-agent",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Capi Guard — a security audit agent for Laravel projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -121,6 +121,15 @@ final class GitHistorySanitizationSkill implements SkillInterface
121
121
  // ── Phase 3: Discover sensitive files in history ─────────────────────
122
122
  $sensitiveFiles = $this->findSensitiveFilesInHistory($repoPath);
123
123
 
124
+ // ── Phase 3b: Deep audit deploy.php files found in history ───────────
125
+ $deployPhpFiles = array_values(array_filter(
126
+ $sensitiveFiles,
127
+ fn (string $f): bool => preg_match('/deploy\.php$/i', $f) === 1
128
+ ));
129
+ $deployPhpAudit = !empty($deployPhpFiles)
130
+ ? $this->auditDeployPhpInHistory($repoPath, $deployPhpFiles)
131
+ : [];
132
+
124
133
  // ── Phase 4: Generate sanitizer script ───────────────────────────────
125
134
  $script = $this->buildSanitizerScript($repoPath, $backupBranch, $sensitiveFiles);
126
135
  file_put_contents($scriptDest, $script);
@@ -140,22 +149,28 @@ final class GitHistorySanitizationSkill implements SkillInterface
140
149
 
141
150
  // ── Build response ────────────────────────────────────────────────────
142
151
  $totalSecrets = array_sum(array_map('count', $secretFindings));
152
+ $deployRisk = array_sum(array_map(
153
+ fn (array $d): int => count($d['server_ips']) + count($d['server_paths']),
154
+ $deployPhpAudit
155
+ ));
143
156
 
144
157
  return [
145
158
  'summary' => implode(' | ', [
146
159
  count($gitignoreFindings) . ' gitignore gaps',
147
160
  $totalSecrets . ' secret occurrences across ' . count($secretFindings) . ' pattern types',
148
161
  count($sensitiveFiles) . ' sensitive files in history',
162
+ count($deployPhpFiles) . ' deploy.php file(s) — ' . $deployRisk . ' server exposure(s)',
149
163
  $dryRun ? 'DRY RUN — script generated only' : 'REWRITE EXECUTED',
150
164
  ]),
151
- 'backup_branch_exists' => $backupExists,
152
- 'dryRun' => $dryRun,
153
- 'gitignore_gaps' => $gitignoreFindings,
154
- 'secret_findings' => $secretFindings,
165
+ 'backup_branch_exists' => $backupExists,
166
+ 'dryRun' => $dryRun,
167
+ 'gitignore_gaps' => $gitignoreFindings,
168
+ 'secret_findings' => $secretFindings,
155
169
  'sensitive_files_history' => $sensitiveFiles,
156
- 'generated_script_path' => $scriptDest,
157
- 'execution_result' => $executionResult,
158
- 'next_steps' => $this->buildNextSteps($backupExists, $backupBranch, $dryRun, $scriptDest),
170
+ 'deploy_php_audit' => $deployPhpAudit,
171
+ 'generated_script_path' => $scriptDest,
172
+ 'execution_result' => $executionResult,
173
+ 'next_steps' => $this->buildNextSteps($backupExists, $backupBranch, $dryRun, $scriptDest),
159
174
  ];
160
175
  }
161
176
 
@@ -294,6 +309,91 @@ final class GitHistorySanitizationSkill implements SkillInterface
294
309
  return array_values($sensitive);
295
310
  }
296
311
 
312
+ /**
313
+ * For each deploy.php found in history, retrieve its blob content and
314
+ * extract any server IP addresses and server folder paths exposed.
315
+ *
316
+ * Uses `git log --all -- <file>` to list commits, then `git show <commit>:<file>`
317
+ * to read the actual file content at that point in time.
318
+ *
319
+ * @param string[] $deployFiles Relative paths matching deploy.php in git history.
320
+ * @return array<int, array{
321
+ * file: string,
322
+ * commit: string,
323
+ * server_ips: string[],
324
+ * server_paths: string[]
325
+ * }>
326
+ */
327
+ private function auditDeployPhpInHistory(string $repoPath, array $deployFiles): array
328
+ {
329
+ // Matches public IPs — excludes loopback (127.), link-local (169.254.),
330
+ // and RFC-1918 private ranges (10., 172.16-31., 192.168.) which are
331
+ // internal infrastructure and not staging/production exposures.
332
+ $ipPattern = '/\b(?!127\.|169\.254\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.)' .
333
+ '(?:\d{1,3}\.){3}\d{1,3}\b/';
334
+
335
+ // Matches common Deployer/Envoy server path keys and bare Unix paths.
336
+ $pathPattern = '/(?:deploy_path|current_path|release_path|app_path|root_path|upload_path)' .
337
+ '\s*[=>\'"]+\s*[\'"]?(\/[^\s\'">,;)]+)' .
338
+ '|(?<![.\w])(?:\/(?:var|home|srv|opt|www|data|sites)\/[^\s\'">,;)]+)/';
339
+
340
+ $results = [];
341
+
342
+ foreach ($deployFiles as $filePath) {
343
+ // Get the most recent commit that touched this file across all branches.
344
+ $logProcess = new Process(
345
+ ['git', 'log', '--all', '--full-history', '--format=%H', '-1', '--', $filePath],
346
+ $repoPath,
347
+ null,
348
+ null,
349
+ 30
350
+ );
351
+ $logProcess->run();
352
+ $commit = trim($logProcess->getOutput());
353
+
354
+ if (empty($commit)) {
355
+ continue;
356
+ }
357
+
358
+ // Read the file blob at that commit.
359
+ $showProcess = new Process(
360
+ ['git', 'show', "{$commit}:{$filePath}"],
361
+ $repoPath,
362
+ null,
363
+ null,
364
+ 30
365
+ );
366
+ $showProcess->run();
367
+ $content = $showProcess->getOutput();
368
+
369
+ if (empty($content)) {
370
+ continue;
371
+ }
372
+
373
+ // Extract IPs.
374
+ preg_match_all($ipPattern, $content, $ipMatches);
375
+ $ips = array_values(array_unique($ipMatches[0]));
376
+
377
+ // Extract server paths.
378
+ preg_match_all($pathPattern, $content, $pathMatches);
379
+ // Group 1 = named key paths, group 2 = bare unix paths.
380
+ $paths = array_values(array_unique(array_filter(
381
+ array_merge($pathMatches[1], $pathMatches[2])
382
+ )));
383
+
384
+ if (!empty($ips) || !empty($paths)) {
385
+ $results[] = [
386
+ 'file' => $filePath,
387
+ 'commit' => substr($commit, 0, 12),
388
+ 'server_ips' => $ips,
389
+ 'server_paths' => $paths,
390
+ ];
391
+ }
392
+ }
393
+
394
+ return $results;
395
+ }
396
+
297
397
  /**
298
398
  * Build the Bash sanitizer script content from the template,
299
399
  * injecting the discovered sensitive file list and secret regexes.
@@ -17,7 +17,7 @@ if git diff --cached --name-only | grep -qE "$BLOCKED_PATTERNS"; then
17
17
  exit 1
18
18
  fi
19
19
 
20
- if git diff --cached | grep -iE "(DB_PASSWORD|APP_KEY|password\s*=\s*['\"][^'\"]{4,}|secret\s*=\s*['\"][^'\"]{4,})"; then
20
+ if git diff --cached | grep -iE "(DB_PASSWORD\s*=\s*\S+|APP_KEY\s*=\s*base64:[A-Za-z0-9+\/=]{40,}|password\s*=\s*['\"][^'\"]{4,}|secret\s*=\s*['\"][^'\"]{4,})"; then
21
21
  echo "❌ BLOCKED: possible hardcoded credential detected in diff"
22
22
  echo " Use environment variables (.env) instead of inline values"
23
23
  exit 1