feat(audit): harden API audit log redaction and schema

- Change query_json column from TEXT to JSON, ip/user_agent to CHAR(64)
  for PII redaction compliance
- Update repository, service, and views for hardened schema
- Add architecture contract test for security logging redaction
- Add search resource provider test coverage
- Include DB migration script for existing installs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 23:20:55 +02:00
parent c2a7d6b789
commit c6c5d06936
14 changed files with 255 additions and 77 deletions

View File

@@ -68,7 +68,13 @@ class ApiAuditLogRepository
RepoQuery::addLikeFilter(
$where,
$params,
['api_audit_log.path', 'api_audit_log.request_id', 'api_audit_log.ip', 'api_audit_log.error_code'],
[
'api_audit_log.path',
'api_audit_log.request_id',
'api_audit_log.error_code',
'api_audit_log.ip',
'api_audit_log.user_agent',
],
$search
);
@@ -331,8 +337,41 @@ class ApiAuditLogRepository
$log['user_email'] = (string) ($user['email'] ?? '');
$log['tenant_uuid'] = (string) ($tenant['uuid'] ?? '');
$log['tenant_description'] = (string) ($tenant['description'] ?? '');
$log['ip_hash'] = trim((string) ($log['ip'] ?? ''));
$log['user_agent_hash'] = trim((string) ($log['user_agent'] ?? ''));
$log['query_keys'] = $this->extractQueryKeys($log['query_json'] ?? null);
return $log;
}
/**
* @return list<string>
*/
private function extractQueryKeys(mixed $queryJson): array
{
if (!is_string($queryJson) || trim($queryJson) === '') {
return [];
}
$decoded = json_decode($queryJson, true);
if (!is_array($decoded)) {
return [];
}
$keysRaw = $decoded['keys'] ?? null;
if (!is_array($keysRaw)) {
return [];
}
$keys = [];
foreach ($keysRaw as $value) {
$key = trim((string) $value);
if ($key === '' || in_array($key, $keys, true)) {
continue;
}
$keys[] = $key;
}
return $keys;
}
}