TenantAtlas/app/Services/ReviewPackService.php
2026-02-23 17:57:29 +01:00

154 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Jobs\GenerateReviewPackJob;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\Tenant;
use App\Models\User;
use App\Support\OperationRunType;
use App\Support\ReviewPackStatus;
use Illuminate\Support\Facades\URL;
class ReviewPackService
{
public function __construct(
private OperationRunService $operationRunService,
) {}
/**
* Create an OperationRun + ReviewPack and dispatch the generation job.
*
* @param array<string, mixed> $options
*/
public function generate(Tenant $tenant, User $user, array $options = []): ReviewPack
{
$options = $this->normalizeOptions($options);
$fingerprint = $this->computeFingerprint($tenant, $options);
$existing = $this->findExistingPack($tenant, $fingerprint);
if ($existing instanceof ReviewPack) {
return $existing;
}
$operationRun = $this->operationRunService->ensureRun(
tenant: $tenant,
type: OperationRunType::ReviewPackGenerate->value,
inputs: [
'include_pii' => $options['include_pii'],
'include_operations' => $options['include_operations'],
],
initiator: $user,
);
$reviewPack = ReviewPack::create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'operation_run_id' => (int) $operationRun->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'status' => ReviewPackStatus::Queued->value,
'options' => $options,
'summary' => [],
]);
$this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void {
GenerateReviewPackJob::dispatch(
reviewPackId: (int) $reviewPack->getKey(),
operationRunId: (int) $operationRun->getKey(),
);
});
return $reviewPack;
}
/**
* Compute a deterministic fingerprint for deduplication.
*
* @param array<string, mixed> $options
*/
public function computeFingerprint(Tenant $tenant, array $options): string
{
$reportFingerprints = StoredReport::query()
->where('tenant_id', (int) $tenant->getKey())
->whereIn('report_type', [
StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
])
->orderBy('report_type')
->pluck('fingerprint')
->toArray();
$maxFindingDate = Finding::query()
->where('tenant_id', (int) $tenant->getKey())
->whereIn('status', [Finding::STATUS_NEW, Finding::STATUS_ACKNOWLEDGED])
->max('updated_at');
$data = [
'tenant_id' => (int) $tenant->getKey(),
'include_pii' => (bool) ($options['include_pii'] ?? true),
'include_operations' => (bool) ($options['include_operations'] ?? true),
'report_fingerprints' => $reportFingerprints,
'max_finding_date' => $maxFindingDate,
'rbac_last_checked_at' => $tenant->rbac_last_checked_at?->toIso8601String(),
];
return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR));
}
/**
* Generate a signed download URL for a review pack.
*/
public function generateDownloadUrl(ReviewPack $pack): string
{
$ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60);
return URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->getKey()],
now()->addMinutes($ttlMinutes),
);
}
/**
* Find an existing ready, non-expired pack with the same fingerprint.
*/
public function findExistingPack(Tenant $tenant, string $fingerprint): ?ReviewPack
{
return ReviewPack::query()
->forTenant((int) $tenant->getKey())
->ready()
->where('fingerprint', $fingerprint)
->where('expires_at', '>', now())
->first();
}
/**
* Check if a generation run is currently active for this tenant.
*/
public function checkActiveRun(Tenant $tenant): bool
{
return OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', OperationRunType::ReviewPackGenerate->value)
->active()
->exists();
}
/**
* @param array<string, mixed> $options
* @return array{include_pii: bool, include_operations: bool}
*/
private function normalizeOptions(array $options): array
{
return [
'include_pii' => (bool) ($options['include_pii'] ?? config('tenantpilot.review_pack.include_pii_default', true)),
'include_operations' => (bool) ($options['include_operations'] ?? config('tenantpilot.review_pack.include_operations_default', true)),
];
}
}