Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
SecurityAuditLogger
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 log
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
8
 logRateLimitExceeded
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 logCsrfViolation
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 logUnauthorizedAccess
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Audit\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Audit\Domain\Model\SecurityEvent;
12use App\Core\Audit\Domain\Model\SecurityEventSeverity;
13use App\Core\Audit\Domain\Model\SecurityEventType;
14use App\Core\Audit\Domain\Repository\SecurityAuditRepositoryInterface;
15use Psr\Http\Message\ServerRequestInterface;
16
17/**
18 * High-Level Security Event Audit Logger.
19 *
20 * Central logging service for security incidents, rate limits, authorization failures,
21 * and CSP violations (NIST SP 800-53 AU-2 / AU-3, OWASP ASVS V7.3).
22 *
23 * @package App\Core\Audit\Application\Service
24 */
25final readonly class SecurityAuditLogger implements SecurityAuditLoggerInterface
26{
27    /**
28     * SecurityAuditLogger constructor.
29     *
30     * @param SecurityAuditRepositoryInterface $repository Repository for security events.
31     * @param AuditDataSanitizer               $sanitizer  Sanitizer for redacting sensitive context.
32     */
33    public function __construct(
34        private SecurityAuditRepositoryInterface $repository,
35        private AuditDataSanitizer               $sanitizer = new AuditDataSanitizer(),
36    ) {
37    }
38
39    /**
40     * Records a security event with full metadata extraction.
41     *
42     * @param SecurityEventType            $type      Classified event type.
43     * @param SecurityEventSeverity        $severity  Severity level.
44     * @param string                       $message   Incident description.
45     * @param array<string, mixed>         $context   Additional metadata context.
46     * @param ServerRequestInterface|null  $request   Optional PSR-7 server request.
47     * @param int|null                     $actorId   Optional actor user ID.
48     * @return int Persisted record ID.
49     */
50    public function log(
51        SecurityEventType       $type,
52        SecurityEventSeverity   $severity,
53        string                  $message,
54        array                   $context = [],
55        ?ServerRequestInterface $request = null,
56        ?int                    $actorId = null
57    ): int {
58        $ip = '127.0.0.1';
59        $ua = null;
60        $uri = null;
61        $method = null;
62        $reqId = null;
63
64        if ($request !== null) {
65            $serverParams = $request->getServerParams();
66            $ip = (string) ($serverParams['REMOTE_ADDR'] ?? '127.0.0.1');
67            $ua = $request->getHeaderLine('User-Agent');
68            $uri = (string) $request->getUri();
69            $method = $request->getMethod();
70            $reqId = $request->getHeaderLine('X-Request-ID');
71            if ($reqId === '') {
72                $reqId = $request->getAttribute('request_id');
73            }
74        }
75
76        $sanitizedContext = $this->sanitizer->sanitize($context);
77
78        $event = new SecurityEvent(
79            eventType:     $type,
80            severity:      $severity,
81            message:       $message,
82            context:       $sanitizedContext,
83            ipAddress:     $ip,
84            actorId:       $actorId,
85            userAgent:     $ua !== '' ? $ua : null,
86            requestUri:    $uri !== '' ? $uri : null,
87            requestMethod: $method !== '' ? $method : null,
88            requestId:     $reqId !== '' && $reqId !== null ? (string) $reqId : null
89        );
90
91        return $this->repository->log($event);
92    }
93
94    /**
95     * Logs rate limit exceeded event (429 Too Many Requests).
96     */
97    public function logRateLimitExceeded(string $ip, string $uri, ?string $requestId = null): int
98    {
99        $event = new SecurityEvent(
100            eventType:     SecurityEventType::RATE_LIMIT_EXCEEDED,
101            severity:      SecurityEventSeverity::MEDIUM,
102            message:       "Rate limit exceeded on URI: {$uri}",
103            context:       ['uri' => $uri],
104            ipAddress:     $ip,
105            requestUri:    $uri,
106            requestId:     $requestId
107        );
108
109        return $this->repository->log($event);
110    }
111
112    /**
113     * Logs CSRF validation violation event (403 Forbidden).
114     */
115    public function logCsrfViolation(
116        string  $ip,
117        string  $uri,
118        ?string $requestId = null,
119        ?int    $actorId = null
120    ): int {
121        $event = new SecurityEvent(
122            eventType:     SecurityEventType::CSRF_VIOLATION,
123            severity:      SecurityEventSeverity::HIGH,
124            message:       "CSRF token mismatch or missing for state-modifying request to {$uri}",
125            context:       ['uri' => $uri],
126            ipAddress:     $ip,
127            actorId:       $actorId,
128            requestUri:    $uri,
129            requestId:     $requestId
130        );
131
132        return $this->repository->log($event);
133    }
134
135    /**
136     * Logs unauthorized access attempt (401 / invalid API token).
137     */
138    public function logUnauthorizedAccess(string $message, ?ServerRequestInterface $request = null): int
139    {
140        return $this->log(
141            SecurityEventType::UNAUTHORIZED_ACCESS,
142            SecurityEventSeverity::MEDIUM,
143            $message,
144            [],
145            $request
146        );
147    }
148}