TenantAtlas/app/Services/Baselines/BaselineAutoCloseService.php
ahmido ec71c2d4e7 feat: harden findings workflow and audit backstop (#181)
## Summary
- harden finding lifecycle changes behind the canonical `FindingWorkflowService` gateway
- route automated resolve and reopen flows through the same audited workflow path
- tighten tenant and workspace scope checks on finding actions and audit visibility
- add focused spec artifacts, workflow regression coverage, automation coverage, and audit visibility tests
- update legacy finding model tests to use the workflow service after direct lifecycle mutators were removed

## Testing
- `vendor/bin/sail bin pint --dirty --format agent`
- focused findings and audit slices passed during implementation
- `vendor/bin/sail artisan test --compact tests/Feature/Models/FindingResolvedTest.php`
- full repository suite passed: `2757 passed`, `8 skipped`, `14448 assertions`

## Notes
- Livewire v4.0+ compliance preserved
- no new Filament assets or panel providers introduced; provider registration remains in `bootstrap/providers.php`
- findings stay on existing Filament action surfaces, with destructive actions still confirmation-gated
- no global search behavior was changed for findings resources

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #181
2026-03-18 12:57:23 +00: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);
}
}