Implements Spec 117 (Golden Master Baseline Drift Engine): - Adds provider-chain resolver for current state hashes (content evidence via PolicyVersion, meta evidence via inventory) - Updates baseline capture + compare jobs to use resolver and persist provenance + fidelity - Adds evidence_fidelity column/index + Filament UI badge/filter/provenance display for findings - Adds performance guard test + integration tests for drift, fidelity semantics, provenance, filter behavior - UX fix: Policies list shows "Sync from Intune" header action only when records exist; empty-state CTA remains and is functional Tests: - `vendor/bin/sail artisan test --compact tests/Feature/Filament/PolicySyncCtaPlacementTest.php` - `vendor/bin/sail artisan test --compact --filter=Baseline` Checklist: - specs/117-baseline-drift-engine/checklists/requirements.md ✓ Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #142
182 lines
6.1 KiB
PHP
182 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Baselines\Evidence;
|
|
|
|
use App\Models\Policy;
|
|
use App\Models\Tenant;
|
|
use App\Services\Baselines\CurrentStateEvidenceProvider;
|
|
use App\Services\Drift\DriftHasher;
|
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class ContentEvidenceProvider implements CurrentStateEvidenceProvider
|
|
{
|
|
public function __construct(
|
|
private readonly DriftHasher $hasher,
|
|
private readonly SettingsNormalizer $settingsNormalizer,
|
|
) {}
|
|
|
|
public function name(): string
|
|
{
|
|
return 'policy_version';
|
|
}
|
|
|
|
public function resolve(Tenant $tenant, array $subjects, ?CarbonImmutable $since = null, ?int $latestInventorySyncRunId = null): array
|
|
{
|
|
if ($subjects === []) {
|
|
return [];
|
|
}
|
|
|
|
$requestedKeys = $this->requestedKeys($subjects);
|
|
|
|
if ($requestedKeys === []) {
|
|
return [];
|
|
}
|
|
|
|
$policyTypes = array_values(array_unique(array_map(
|
|
static fn (array $subject): string => trim((string) ($subject['policy_type'] ?? '')),
|
|
$subjects,
|
|
)));
|
|
$policyTypes = array_values(array_filter($policyTypes, static fn (string $value): bool => $value !== ''));
|
|
|
|
$externalIds = array_values(array_unique(array_map(
|
|
static fn (array $subject): string => trim((string) ($subject['subject_external_id'] ?? '')),
|
|
$subjects,
|
|
)));
|
|
$externalIds = array_values(array_filter($externalIds, static fn (string $value): bool => $value !== ''));
|
|
|
|
if ($policyTypes === [] || $externalIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$policies = Policy::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->whereIn('policy_type', $policyTypes)
|
|
->whereIn('external_id', $externalIds)
|
|
->get(['id', 'policy_type', 'external_id']);
|
|
|
|
/** @var Collection<int, Policy> $policies */
|
|
$policyIdToKey = [];
|
|
$policyIds = [];
|
|
|
|
foreach ($policies as $policy) {
|
|
if (! $policy instanceof Policy) {
|
|
continue;
|
|
}
|
|
|
|
$key = (string) $policy->policy_type.'|'.(string) $policy->external_id;
|
|
|
|
if (! array_key_exists($key, $requestedKeys)) {
|
|
continue;
|
|
}
|
|
|
|
$policyIdToKey[(int) $policy->getKey()] = $key;
|
|
$policyIds[] = (int) $policy->getKey();
|
|
}
|
|
|
|
if ($policyIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$baseQuery = DB::table('policy_versions')
|
|
->select([
|
|
'policy_versions.id',
|
|
'policy_versions.policy_id',
|
|
'policy_versions.policy_type',
|
|
'policy_versions.platform',
|
|
'policy_versions.captured_at',
|
|
'policy_versions.snapshot',
|
|
'policy_versions.version_number',
|
|
])
|
|
->selectRaw('ROW_NUMBER() OVER (PARTITION BY policy_id ORDER BY captured_at DESC, version_number DESC, id DESC) as rn')
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->whereIn('policy_id', $policyIds)
|
|
->whereNull('deleted_at');
|
|
|
|
if ($since instanceof CarbonImmutable) {
|
|
$baseQuery->where('captured_at', '>=', $since->toDateTimeString());
|
|
}
|
|
|
|
$versions = DB::query()
|
|
->fromSub($baseQuery, 'ranked_policy_versions')
|
|
->where('rn', 1)
|
|
->get();
|
|
|
|
$resolved = [];
|
|
|
|
foreach ($versions as $version) {
|
|
$policyId = is_numeric($version->policy_id ?? null) ? (int) $version->policy_id : null;
|
|
$key = $policyId !== null ? ($policyIdToKey[$policyId] ?? null) : null;
|
|
|
|
if (! is_string($key) || $key === '' || ! array_key_exists($key, $requestedKeys)) {
|
|
continue;
|
|
}
|
|
|
|
$policyType = is_string($version->policy_type ?? null) ? (string) $version->policy_type : '';
|
|
$subjectExternalId = (string) ($requestedKeys[$key] ?? '');
|
|
|
|
if ($policyType === '' || $subjectExternalId === '') {
|
|
continue;
|
|
}
|
|
|
|
$snapshot = $version->snapshot ?? null;
|
|
$snapshot = is_array($snapshot) ? $snapshot : (is_string($snapshot) ? json_decode($snapshot, true) : null);
|
|
$snapshot = is_array($snapshot) ? $snapshot : [];
|
|
|
|
$platform = is_string($version->platform ?? null) ? (string) $version->platform : null;
|
|
|
|
$normalized = $this->settingsNormalizer->normalizeForDiff(
|
|
snapshot: $snapshot,
|
|
policyType: $policyType,
|
|
platform: $platform,
|
|
);
|
|
|
|
$hash = $this->hasher->hashNormalized($normalized);
|
|
|
|
$observedAt = is_string($version->captured_at ?? null) ? CarbonImmutable::parse((string) $version->captured_at) : null;
|
|
$policyVersionId = is_numeric($version->id ?? null) ? (int) $version->id : null;
|
|
|
|
$resolved[$key] = new ResolvedEvidence(
|
|
policyType: $policyType,
|
|
subjectExternalId: $subjectExternalId,
|
|
hash: $hash,
|
|
fidelity: EvidenceProvenance::FidelityContent,
|
|
source: EvidenceProvenance::SourcePolicyVersion,
|
|
observedAt: $observedAt,
|
|
observedOperationRunId: null,
|
|
meta: [
|
|
'policy_version_id' => $policyVersionId,
|
|
],
|
|
);
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{policy_type: string, subject_external_id: string}> $subjects
|
|
* @return array<string, string>
|
|
*/
|
|
private function requestedKeys(array $subjects): array
|
|
{
|
|
$keys = [];
|
|
|
|
foreach ($subjects as $subject) {
|
|
$policyType = trim((string) ($subject['policy_type'] ?? ''));
|
|
$externalId = trim((string) ($subject['subject_external_id'] ?? ''));
|
|
|
|
if ($policyType === '' || $externalId === '') {
|
|
continue;
|
|
}
|
|
|
|
$keys[$policyType.'|'.$externalId] = $externalId;
|
|
}
|
|
|
|
return $keys;
|
|
}
|
|
}
|