presseportale/app/Services/Api/LegacyApiAccessLogAnalyzer.php
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

184 lines
5.8 KiB
PHP

<?php
namespace App\Services\Api;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class LegacyApiAccessLogAnalyzer
{
/**
* @param list<string> $paths
* @return array<string, mixed>
*/
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<string> $paths
* @return list<string>
*/
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<client_ip>\S+).*"(?P<method>GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(?P<target>\S+)[^"]*"\s+(?P<status>\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<string, mixed> $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<string, mixed> $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);
}
}
}