TenantAtlas/app/Services/Baselines/BaselineAutoCloseService.php
2026-03-18 13:56:09 +01:00

134 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Baselines;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\Workspace;
use App\Services\Findings\FindingWorkflowService;
use App\Services\Settings\SettingsResolver;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Carbon\CarbonImmutable;
final class BaselineAutoCloseService
{
public function __construct(
private readonly SettingsResolver $settingsResolver,
private readonly ?FindingWorkflowService $findingWorkflowService = null,
) {}
public function shouldAutoClose(Tenant $tenant, OperationRun $run): bool
{
if ($run->status !== OperationRunStatus::Completed->value) {
return false;
}
if ($run->outcome !== OperationRunOutcome::Succeeded->value) {
return false;
}
$summaryCounts = is_array($run->summary_counts) ? $run->summary_counts : [];
if (
! array_key_exists('total', $summaryCounts)
|| ! array_key_exists('processed', $summaryCounts)
|| ! array_key_exists('failed', $summaryCounts)
) {
return false;
}
$total = (int) $summaryCounts['total'];
$processed = (int) $summaryCounts['processed'];
$failed = (int) $summaryCounts['failed'];
if ($processed !== $total || $failed !== 0) {
return false;
}
$workspace = $this->resolveWorkspace($tenant);
if (! $workspace instanceof Workspace) {
return false;
}
try {
return (bool) $this->settingsResolver->resolveValue(
workspace: $workspace,
domain: 'baseline',
key: 'auto_close_enabled',
);
} catch (\InvalidArgumentException) {
return false;
}
}
/**
* @param array<int, string> $seenFingerprints
*/
public function resolveStaleFindings(
Tenant $tenant,
int $baselineProfileId,
array $seenFingerprints,
int $currentOperationRunId,
): int {
$scopeKey = 'baseline_profile:'.$baselineProfileId;
$resolvedAt = CarbonImmutable::now();
$resolvedCount = 0;
$query = Finding::query()
->where('tenant_id', (int) $tenant->getKey())
->where('finding_type', Finding::FINDING_TYPE_DRIFT)
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->whereIn('status', Finding::openStatusesForQuery())
->orderBy('id');
if ($seenFingerprints !== []) {
$query->whereNotIn('fingerprint', array_values(array_unique($seenFingerprints)));
}
$query->chunkById(100, function ($findings) use ($tenant, &$resolvedCount, $resolvedAt, $currentOperationRunId): void {
foreach ($findings as $finding) {
if (! $finding instanceof Finding) {
continue;
}
$this->findingWorkflowService()->resolveBySystem(
finding: $finding,
tenant: $tenant,
reason: 'no_longer_drifting',
resolvedAt: $resolvedAt,
operationRunId: $currentOperationRunId,
mutate: function (Finding $record) use ($currentOperationRunId): void {
$record->current_operation_run_id = $currentOperationRunId;
},
);
$resolvedCount++;
}
});
return $resolvedCount;
}
private function resolveWorkspace(Tenant $tenant): ?Workspace
{
$workspaceId = (int) ($tenant->workspace_id ?? 0);
if ($workspaceId <= 0) {
return null;
}
return Workspace::query()->whereKey($workspaceId)->first();
}
private function findingWorkflowService(): FindingWorkflowService
{
return $this->findingWorkflowService ?? app(FindingWorkflowService::class);
}
}