Files
Investidor-V2/app/Services/Malware/DocumentScanAttempts.php
T

77 lines
4.6 KiB
PHP

<?php
namespace App\Services\Malware;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use stdClass;
/** Attempt ownership in the existing audit ledger; callers hold the document row lock. */
final class DocumentScanAttempts
{
private const SOURCE = 'document-malware-worker';
public function owns(stdClass $document, string $run, int $delivery, ?int $expectedAttempt = null): bool
{
$attempt = (int) $document->malware_scan_attempts;
if ($expectedAttempt !== null && $attempt !== $expectedAttempt) {
return false;
}
$events = DB::table('audit_events')->where('source_system', self::SOURCE)->where('entity_type', 'document-scan')->where('entity_id', $document->id)
->where('action', 'scan_started')->whereRaw("after_data->>'attempt' = ?", [(string) $attempt])->get(['after_data']);
if ($events->count() !== 1) {
return false;
}
$lease = json_decode($events->first()->after_data, true, 512, JSON_THROW_ON_ERROR);
return is_array($lease) && ($lease['run_id'] ?? null) === $run && ($lease['delivery_attempt'] ?? null) === $delivery && ($lease['attempt'] ?? null) === $attempt;
}
public function start(stdClass $document, string $run, int $delivery): int
{
$attempt = (int) $document->malware_scan_attempts + 1;
DB::table('audit_events')->insert(['id' => (string) Str::uuid(), 'source_system' => self::SOURCE, 'entity_type' => 'document-scan', 'entity_id' => $document->id,
'actor_user_id' => null, 'action' => 'scan_started', 'correlation_id' => $run, 'data_classification' => 'restricted',
'after_data' => json_encode(['run_id' => $run, 'delivery_attempt' => $delivery, 'attempt' => $attempt, 'file_sha256' => trim($document->sha256), 'file_size_bytes' => $document->size_bytes ?? null], JSON_THROW_ON_ERROR)]);
return $attempt;
}
/** @return array{sha256:string,size_bytes:int|null}|null */
public function descriptor(stdClass $document, string $run, int $delivery): ?array
{
if (! $this->owns($document, $run, $delivery)) {
return null;
}
$event = DB::table('audit_events')->where('source_system', self::SOURCE)->where('entity_type', 'document-scan')->where('entity_id', $document->id)
->where('action', 'scan_started')->whereRaw("after_data->>'attempt' = ?", [(string) $document->malware_scan_attempts])->first(['after_data']);
$data = $event ? json_decode($event->after_data, true, 512, JSON_THROW_ON_ERROR) : null;
if (! is_array($data) || ! is_string($data['file_sha256'] ?? null) || preg_match('/^[a-f0-9]{64}$/D', $data['file_sha256']) !== 1
|| ! array_key_exists('file_size_bytes', $data) || ($data['file_size_bytes'] !== null && ! is_int($data['file_size_bytes']))) {
return null;
}
return ['sha256' => $data['file_sha256'], 'size_bytes' => $data['file_size_bytes']];
}
public function completed(string $documentId, string $run): bool
{
return DB::table('audit_events')->where('source_system', self::SOURCE)->where('entity_type', 'document-scan')->where('entity_id', $documentId)
->where('action', 'scan_result_recorded')->where('correlation_id', $run)->whereRaw("after_data->>'retryable_failure' = 'false'")->exists();
}
public function finish(stdClass $document, string $run, int $delivery, string $scanId, bool $retryableFailure): void
{
DB::table('audit_events')->insert(['id' => (string) Str::uuid(), 'source_system' => self::SOURCE, 'entity_type' => 'document-scan', 'entity_id' => $document->id,
'actor_user_id' => null, 'action' => 'scan_result_recorded', 'correlation_id' => $run, 'data_classification' => 'restricted',
'after_data' => json_encode(['run_id' => $run, 'delivery_attempt' => $delivery, 'attempt' => (int) $document->malware_scan_attempts, 'scan_id' => $scanId, 'retryable_failure' => $retryableFailure], JSON_THROW_ON_ERROR)]);
}
public function discarded(stdClass $document, string $run, int $delivery, int $expectedAttempt): void
{
DB::table('audit_events')->insert(['id' => (string) Str::uuid(), 'source_system' => self::SOURCE, 'entity_type' => 'document-scan', 'entity_id' => $document->id,
'actor_user_id' => null, 'action' => 'scan_result_discarded', 'correlation_id' => $run, 'data_classification' => 'restricted',
'after_data' => json_encode(['run_id' => $run, 'delivery_attempt' => $delivery, 'attempt' => $expectedAttempt, 'current_attempt' => (int) $document->malware_scan_attempts], JSON_THROW_ON_ERROR)]);
}
}