$paths * @return array */ public function analyze(array $paths, int $top = 20): array { $report = [ 'generated_at' => now()->toIso8601String(), 'input_paths' => $paths, 'missing_paths' => [], 'summary' => [ 'files' => 0, 'total_lines' => 0, 'matched_requests' => 0, 'legacy_key_requests' => 0, 'unique_client_ips' => 0, 'unique_api_key_fingerprints' => 0, ], 'endpoints' => [], 'client_ips' => [], 'api_key_fingerprints' => [], 'status_codes' => [], 'sample_requests' => [], ]; foreach ($this->expandPaths($paths) as $path) { if (! is_readable($path)) { $report['missing_paths'][] = $path; continue; } $report['summary']['files']++; $file = new \SplFileObject($path); while (! $file->eof()) { $line = trim((string) $file->fgets()); if ($line === '') { continue; } $report['summary']['total_lines']++; $request = $this->parseLine($line); if ($request === null) { continue; } $endpoint = $this->legacyEndpoint($request['path']); if ($endpoint === null) { continue; } $this->recordMatch($report, $request, $endpoint); } } $report['summary']['unique_client_ips'] = count($report['client_ips']); $report['summary']['unique_api_key_fingerprints'] = count($report['api_key_fingerprints']); $this->sortAndLimit($report, $top); return $report; } /** * @param list $paths * @return list */ private function expandPaths(array $paths): array { $expanded = []; foreach ($paths as $path) { $matches = Str::contains($path, ['*', '?', '[']) ? glob($path) ?: [] : [$path]; foreach ($matches as $match) { $expanded[] = $match; } } return array_values(array_unique($expanded)); } /** * @return array{client_ip: string, method: string, target: string, path: string, query: string, status: string|null}|null */ private function parseLine(string $line): ?array { if (! preg_match('/^(?P\S+).*"(?PGET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(?P\S+)[^"]*"\s+(?P\d{3}|-)?/', $line, $matches)) { return null; } $target = $matches['target']; $parsedUrl = parse_url($target); $path = Arr::get($parsedUrl, 'path', Str::before($target, '?')); $query = Arr::get($parsedUrl, 'query', Str::contains($target, '?') ? Str::after($target, '?') : ''); return [ 'client_ip' => $matches['client_ip'], 'method' => $matches['method'], 'target' => $target, 'path' => $path, 'query' => $query, 'status' => $matches['status'] !== '-' ? ($matches['status'] ?? null) : null, ]; } private function legacyEndpoint(string $path): ?string { $normalizedPath = Str::of($path) ->replace('/index.php', '') ->trim('/') ->toString(); if (! preg_match('~(?:^|/)(pressrelease|pressreleaseimage|company|category|newsletter)/([a-zA-Z0-9_-]+)~', $normalizedPath, $matches)) { return null; } return "{$matches[1]}/{$matches[2]}"; } /** * @param array $report * @param array{client_ip: string, method: string, target: string, path: string, query: string, status: string|null} $request */ private function recordMatch(array &$report, array $request, string $endpoint): void { $report['summary']['matched_requests']++; $report['endpoints'][$endpoint] = ($report['endpoints'][$endpoint] ?? 0) + 1; $report['client_ips'][$request['client_ip']] = ($report['client_ips'][$request['client_ip']] ?? 0) + 1; if ($request['status'] !== null) { $report['status_codes'][$request['status']] = ($report['status_codes'][$request['status']] ?? 0) + 1; } parse_str($request['query'], $query); if (filled($query['api_key'] ?? null)) { $report['summary']['legacy_key_requests']++; $fingerprint = hash('sha256', (string) $query['api_key']); $report['api_key_fingerprints'][$fingerprint] = ($report['api_key_fingerprints'][$fingerprint] ?? 0) + 1; } if (count($report['sample_requests']) < 10) { $report['sample_requests'][] = [ 'method' => $request['method'], 'target' => $this->maskApiKey($request['target']), 'endpoint' => $endpoint, 'client_ip' => $request['client_ip'], 'status' => $request['status'], ]; } } private function maskApiKey(string $target): string { return preg_replace('/([?&]api_key=)[^&]+/', '$1***', $target) ?? $target; } /** * @param array $report */ private function sortAndLimit(array &$report, int $top): void { foreach (['endpoints', 'client_ips', 'api_key_fingerprints', 'status_codes'] as $key) { arsort($report[$key]); $report[$key] = array_slice($report[$key], 0, $top, true); } } }