TenantAtlas/app/Services/Drift/DriftFindingGenerator.php
2026-01-13 23:55:41 +01:00

128 lines
4.9 KiB
PHP

<?php
namespace App\Services\Drift;
use App\Models\Finding;
use App\Models\InventorySyncRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use Illuminate\Support\Arr;
use RuntimeException;
class DriftFindingGenerator
{
public function __construct(
private readonly DriftHasher $hasher,
private readonly DriftEvidence $evidence,
) {}
public function generate(Tenant $tenant, InventorySyncRun $baseline, InventorySyncRun $current, string $scopeKey): int
{
if (! $baseline->finished_at || ! $current->finished_at) {
throw new RuntimeException('Baseline/current run must be finished.');
}
/** @var array<string, mixed> $selection */
$selection = is_array($current->selection_payload) ? $current->selection_payload : [];
$policyTypes = Arr::get($selection, 'policy_types');
if (! is_array($policyTypes)) {
$policyTypes = [];
}
$policyTypes = array_values(array_filter(array_map('strval', $policyTypes)));
$created = 0;
Policy::query()
->where('tenant_id', $tenant->getKey())
->whereIn('policy_type', $policyTypes)
->orderBy('id')
->chunk(200, function ($policies) use ($tenant, $baseline, $current, $scopeKey, &$created): void {
foreach ($policies as $policy) {
if (! $policy instanceof Policy) {
continue;
}
$baselineVersion = $this->versionForRun($policy, $baseline);
$currentVersion = $this->versionForRun($policy, $current);
if (! $baselineVersion instanceof PolicyVersion || ! $currentVersion instanceof PolicyVersion) {
continue;
}
$baselineAssignmentsHash = $baselineVersion->assignments_hash ?? null;
$currentAssignmentsHash = $currentVersion->assignments_hash ?? null;
if ($baselineAssignmentsHash === $currentAssignmentsHash) {
continue;
}
$fingerprint = $this->hasher->fingerprint(
tenantId: (int) $tenant->getKey(),
scopeKey: $scopeKey,
subjectType: 'assignment',
subjectExternalId: (string) $policy->external_id,
changeType: 'modified',
baselineHash: (string) ($baselineAssignmentsHash ?? ''),
currentHash: (string) ($currentAssignmentsHash ?? ''),
);
$rawEvidence = [
'change_type' => 'modified',
'summary' => 'Policy assignments changed',
'baseline' => [
'policy_id' => $policy->external_id,
'policy_version_id' => $baselineVersion->getKey(),
'assignments_hash' => $baselineAssignmentsHash,
],
'current' => [
'policy_id' => $policy->external_id,
'policy_version_id' => $currentVersion->getKey(),
'assignments_hash' => $currentAssignmentsHash,
],
];
Finding::query()->updateOrCreate(
[
'tenant_id' => $tenant->getKey(),
'fingerprint' => $fingerprint,
],
[
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'scope_key' => $scopeKey,
'baseline_run_id' => $baseline->getKey(),
'current_run_id' => $current->getKey(),
'subject_type' => 'assignment',
'subject_external_id' => (string) $policy->external_id,
'severity' => Finding::SEVERITY_MEDIUM,
'status' => Finding::STATUS_NEW,
'acknowledged_at' => null,
'acknowledged_by_user_id' => null,
'evidence_jsonb' => $this->evidence->sanitize($rawEvidence),
],
);
$created++;
}
});
return $created;
}
private function versionForRun(Policy $policy, InventorySyncRun $run): ?PolicyVersion
{
if (! $run->finished_at) {
return null;
}
return PolicyVersion::query()
->where('tenant_id', $policy->tenant_id)
->where('policy_id', $policy->getKey())
->where('captured_at', '<=', $run->finished_at)
->latest('captured_at')
->first();
}
}