## Summary - add Intune RBAC Role Definition baseline scope support, capture references, compare classification, findings evidence, and landing/detail UI labels - keep Intune Role Assignments explicitly excluded from baseline compare scope, summaries, findings, and restore messaging - add focused Pest coverage for baseline scope selection, capture, compare behavior, recurrence, isolation, findings rendering, inventory anchoring, and RBAC summaries ## Verification - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Unit/Inventory/InventoryPolicyTypeMetaBaselineSupportTest.php tests/Unit/Baselines/BaselinePolicyVersionResolverTest.php tests/Unit/Baselines/BaselineScopeTest.php tests/Unit/IntuneRoleDefinitionNormalizerTest.php tests/Feature/Baselines/BaselineCaptureRbacRoleDefinitionsTest.php tests/Feature/Baselines/BaselineCompareRbacRoleDefinitionsTest.php tests/Feature/Baselines/BaselineCompareDriftEvidenceContractRbacTest.php tests/Feature/Baselines/BaselineCompareCoverageGuardTest.php tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php tests/Feature/Baselines/BaselineCompareFindingRecurrenceKeyTest.php tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php tests/Feature/Filament/BaselineProfileFoundationScopeTest.php tests/Feature/Filament/BaselineSnapshotRbacRoleDefinitionsTest.php tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php tests/Feature/Filament/FindingViewRbacEvidenceTest.php tests/Feature/Findings/FindingRecurrenceTest.php tests/Feature/Findings/DriftStaleAutoResolveTest.php tests/Feature/Inventory/InventorySyncButtonTest.php tests/Feature/Inventory/InventorySyncServiceTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php` - result: `71 passed (467 assertions)` ## Filament / Platform Notes - Livewire compliance: unchanged and compatible with Livewire v4.0+ - Provider registration: no panel/provider changes; `bootstrap/providers.php` remains the registration location - Global search: no new globally searchable resource added; existing global search behavior is unchanged - Destructive actions: no new destructive actions introduced; existing confirmed actions remain unchanged - Assets: no new Filament assets introduced; deploy asset handling remains unchanged, including `php artisan filament:assets` - Testing plan covered: baseline profile scope, snapshot detail, compare job, findings recurrence, findings detail, compare landing labels, inventory sync anchoring, and tenant isolation Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #156
148 lines
4.0 KiB
PHP
148 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Baselines\Evidence;
|
|
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use App\Support\Baselines\BaselineSubjectKey;
|
|
use Carbon\CarbonImmutable;
|
|
use Throwable;
|
|
|
|
final class BaselinePolicyVersionResolver
|
|
{
|
|
/**
|
|
* Cached map of (tenant_id, policy_type) => subject_key => policy_id.
|
|
*
|
|
* @var array<int, array<string, array<string, int>>>
|
|
*/
|
|
private array $policyIdIndex = [];
|
|
|
|
public function resolve(
|
|
Tenant $tenant,
|
|
string $policyType,
|
|
string $subjectKey,
|
|
?string $observedAt,
|
|
): ?int {
|
|
$tenantId = (int) $tenant->getKey();
|
|
|
|
$policyType = trim($policyType);
|
|
$subjectKey = trim($subjectKey);
|
|
|
|
if ($tenantId <= 0 || $policyType === '' || $subjectKey === '') {
|
|
return null;
|
|
}
|
|
|
|
$observedAtCarbon = $this->parseObservedAt($observedAt);
|
|
|
|
if (! $observedAtCarbon instanceof CarbonImmutable) {
|
|
return null;
|
|
}
|
|
|
|
$policyId = $this->resolvePolicyId($tenantId, $policyType, $subjectKey);
|
|
|
|
if ($policyId === null) {
|
|
return null;
|
|
}
|
|
|
|
$rangeStart = $observedAtCarbon;
|
|
$rangeEnd = $observedAtCarbon->addSecond();
|
|
|
|
$versionId = PolicyVersion::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('policy_id', $policyId)
|
|
->whereNull('deleted_at')
|
|
->where('captured_at', '>=', $rangeStart)
|
|
->where('captured_at', '<', $rangeEnd)
|
|
->orderByDesc('captured_at')
|
|
->orderByDesc('version_number')
|
|
->orderByDesc('id')
|
|
->value('id');
|
|
|
|
return is_numeric($versionId) ? (int) $versionId : null;
|
|
}
|
|
|
|
private function parseObservedAt(?string $observedAt): ?CarbonImmutable
|
|
{
|
|
if (! is_string($observedAt)) {
|
|
return null;
|
|
}
|
|
|
|
$observedAt = trim($observedAt);
|
|
|
|
if ($observedAt === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return CarbonImmutable::parse($observedAt);
|
|
} catch (Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function resolvePolicyId(int $tenantId, string $policyType, string $subjectKey): ?int
|
|
{
|
|
if (! array_key_exists($tenantId, $this->policyIdIndex) || ! array_key_exists($policyType, $this->policyIdIndex[$tenantId])) {
|
|
$this->policyIdIndex[$tenantId][$policyType] = $this->buildIndex($tenantId, $policyType);
|
|
}
|
|
|
|
$policyId = $this->policyIdIndex[$tenantId][$policyType][$subjectKey] ?? null;
|
|
|
|
return is_numeric($policyId) ? (int) $policyId : null;
|
|
}
|
|
|
|
/**
|
|
* Build a subject_key => policy_id map for a given tenant + policy_type.
|
|
*
|
|
* If multiple policies map to the same subject_key, that key is treated as ambiguous and excluded.
|
|
*
|
|
* @return array<string, int>
|
|
*/
|
|
private function buildIndex(int $tenantId, string $policyType): array
|
|
{
|
|
$policies = Policy::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('policy_type', $policyType)
|
|
->get(['id', 'display_name', 'external_id']);
|
|
|
|
/** @var array<string, int> $index */
|
|
$index = [];
|
|
|
|
/** @var array<string, true> $ambiguous */
|
|
$ambiguous = [];
|
|
|
|
foreach ($policies as $policy) {
|
|
if (! $policy instanceof Policy) {
|
|
continue;
|
|
}
|
|
|
|
$key = BaselineSubjectKey::forPolicy(
|
|
$policyType,
|
|
is_string($policy->display_name) ? $policy->display_name : null,
|
|
is_string($policy->external_id) ? $policy->external_id : null,
|
|
);
|
|
|
|
if ($key === null) {
|
|
continue;
|
|
}
|
|
|
|
if (array_key_exists($key, $index)) {
|
|
$ambiguous[$key] = true;
|
|
|
|
continue;
|
|
}
|
|
|
|
$index[$key] = (int) $policy->getKey();
|
|
}
|
|
|
|
foreach (array_keys($ambiguous) as $key) {
|
|
unset($index[$key]);
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
}
|