TenantAtlas/tests/Feature/Baselines/BaselineCompareFindingsTest.php
ahmido da1adbdeb5 Spec 119: Drift cutover to Baseline Compare (golden master) (#144)
Implements Spec 119 (Drift Golden Master Cutover):

- Baseline Compare is the only drift writer (`source = baseline.compare`).
- Drift findings now store diff-compatible `evidence_jsonb` (summary.kind, baseline/current policy_version_id refs, fidelity + provenance).
- Findings UI renders one-sided diffs for `missing_policy`/`unexpected_policy` when a single ref exists; otherwise shows explicit “diff unavailable”.
- Removes legacy drift generator runtime (jobs/services/UI) and related tests.
- Adds one-time migration to delete legacy drift findings (`finding_type=drift` where source is null or != baseline.compare).
- Scopes baseline capture & landing duplicate warnings to latest completed inventory sync.
- Canonicalizes compliance `scheduledActionsForRule` drift signal and keeps legacy snapshots comparable.

Tests:
- `vendor/bin/sail artisan test --compact` (full suite per tasks)
- Focused pack: BaselinePolicyVersionResolverTest, BaselineCompareDriftEvidenceContractTest, DriftFindingDiffUnavailableTest, LegacyDriftFindingsCleanupMigrationTest, ComplianceNoncomplianceActionsDriftTest

Notes:
- Livewire v4+ / Filament v5 compatible (no legacy APIs).
- No new external dependencies.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #144
2026-03-06 14:30:49 +00:00

1411 lines
54 KiB
PHP

<?php
use App\Jobs\CaptureBaselineSnapshotJob;
use App\Jobs\CompareBaselineToTenantJob;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineSnapshotItem;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\WorkspaceSetting;
use App\Services\Baselines\BaselineSnapshotIdentity;
use App\Services\Baselines\InventoryMetaContract;
use App\Services\Drift\DriftHasher;
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Services\Settings\SettingsResolver;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\OperationRunType;
use App\Support\OpsUx\OperationSummaryKeys;
use function Pest\Laravel\mock;
// --- T041: Compare idempotent finding fingerprint tests ---
it('creates drift findings when baseline and tenant inventory differ', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$policyType = 'deviceConfiguration';
$displayNameA = 'Policy A';
$subjectKeyA = BaselineSubjectKey::fromDisplayName($displayNameA);
expect($subjectKeyA)->not->toBeNull();
$workspaceSafeExternalIdA = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKeyA);
$baselineHashA = app(BaselineSnapshotIdentity::class)->hashItemContent(
policyType: $policyType,
subjectExternalId: 'policy-a-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE_A'],
);
$displayNameB = 'Policy B';
$subjectKeyB = BaselineSubjectKey::fromDisplayName($displayNameB);
expect($subjectKeyB)->not->toBeNull();
$workspaceSafeExternalIdB = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKeyB);
$baselineHashB = app(BaselineSnapshotIdentity::class)->hashItemContent(
policyType: $policyType,
subjectExternalId: 'policy-b-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE_B'],
);
// Baseline has policyA and policyB
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalIdA,
'subject_key' => (string) $subjectKeyA,
'policy_type' => $policyType,
'baseline_hash' => $baselineHashA,
'meta_jsonb' => ['display_name' => $displayNameA],
]);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalIdB,
'subject_key' => (string) $subjectKeyB,
'policy_type' => $policyType,
'baseline_hash' => $baselineHashB,
'meta_jsonb' => ['display_name' => $displayNameB],
]);
// Tenant has policyA (different content) and policyC (unexpected)
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'policy-a-uuid',
'policy_type' => $policyType,
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_A'],
'display_name' => $displayNameA,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'policy-c-uuid',
'policy_type' => $policyType,
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_C'],
'display_name' => 'Policy C',
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
$job = new CompareBaselineToTenantJob($run);
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run->refresh();
expect($run->status)->toBe('completed');
expect($run->outcome)->toBe('succeeded');
$context = is_array($run->context) ? $run->context : [];
$countsByChangeType = $context['findings']['counts_by_change_type'] ?? null;
expect($countsByChangeType)->toBe([
'different_version' => 1,
'missing_policy' => 1,
'unexpected_policy' => 1,
]);
$scopeKey = 'baseline_profile:'.$profile->getKey();
$findings = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->get();
// policyB missing (high), policyA different (medium), policyC unexpected (low) = 3 findings
expect($findings->count())->toBe(3);
expect($findings->every(fn (Finding $finding): bool => filled($finding->recurrence_key) && $finding->recurrence_key === $finding->fingerprint))->toBeTrue();
// Lifecycle v2 fields must be initialized for new findings.
expect($findings->pluck('first_seen_at')->filter()->count())->toBe($findings->count());
expect($findings->pluck('last_seen_at')->filter()->count())->toBe($findings->count());
expect($findings->pluck('times_seen')->every(fn ($value) => (int) $value === 1))->toBeTrue();
$severities = $findings->pluck('severity')->sort()->values()->all();
expect($severities)->toContain(Finding::SEVERITY_HIGH);
expect($severities)->toContain(Finding::SEVERITY_MEDIUM);
expect($severities)->toContain(Finding::SEVERITY_LOW);
});
it('does not fail compare when baseline severity mapping setting is missing', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$policyType = 'deviceConfiguration';
$displayNameA = 'Policy A';
$subjectKeyA = BaselineSubjectKey::fromDisplayName($displayNameA);
expect($subjectKeyA)->not->toBeNull();
$workspaceSafeExternalIdA = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKeyA);
$baselineHashA = app(BaselineSnapshotIdentity::class)->hashItemContent(
policyType: $policyType,
subjectExternalId: 'policy-a-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE_A'],
);
$displayNameB = 'Policy B';
$subjectKeyB = BaselineSubjectKey::fromDisplayName($displayNameB);
expect($subjectKeyB)->not->toBeNull();
$workspaceSafeExternalIdB = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKeyB);
$baselineHashB = app(BaselineSnapshotIdentity::class)->hashItemContent(
policyType: $policyType,
subjectExternalId: 'policy-b-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE_B'],
);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalIdA,
'subject_key' => (string) $subjectKeyA,
'policy_type' => $policyType,
'baseline_hash' => $baselineHashA,
'meta_jsonb' => ['display_name' => $displayNameA],
]);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalIdB,
'subject_key' => (string) $subjectKeyB,
'policy_type' => $policyType,
'baseline_hash' => $baselineHashB,
'meta_jsonb' => ['display_name' => $displayNameB],
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'policy-a-uuid',
'policy_type' => $policyType,
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_A'],
'display_name' => $displayNameA,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
$realSettingsResolver = app(SettingsResolver::class);
$settingsResolver = mock(SettingsResolver::class);
$settingsResolver
->shouldReceive('resolveValue')
->andReturnUsing(function ($workspace, $domain, $key, $tenant = null) use ($realSettingsResolver): mixed {
if ($domain === 'baseline' && $key === 'severity_mapping') {
throw new InvalidArgumentException('Unknown setting key: baseline.severity_mapping');
}
return $realSettingsResolver->resolveValue($workspace, $domain, $key, $tenant);
});
$baselineAutoCloseService = new \App\Services\Baselines\BaselineAutoCloseService($settingsResolver);
(new CompareBaselineToTenantJob($run))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
settingsResolver: $settingsResolver,
baselineAutoCloseService: $baselineAutoCloseService,
);
$run->refresh();
expect($run->status)->toBe('completed');
expect($run->outcome)->toBe('succeeded');
$scopeKey = 'baseline_profile:'.$profile->getKey();
$findings = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->get();
// policyB missing (high), policyA different (medium) = 2 findings.
expect($findings->count())->toBe(2);
expect($findings->pluck('severity')->all())->toContain(Finding::SEVERITY_HIGH);
expect($findings->pluck('severity')->all())->toContain(Finding::SEVERITY_MEDIUM);
});
it('treats inventory items not seen in latest inventory sync as missing', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['settingsCatalogPolicy']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$olderInventoryRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['settingsCatalogPolicy' => 'succeeded'],
attributes: [
'selection_hash' => 'older',
'selection_payload' => ['policy_types' => ['settingsCatalogPolicy']],
'finished_at' => now()->subMinutes(5),
],
);
createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['settingsCatalogPolicy' => 'succeeded'],
attributes: [
'selection_hash' => 'latest',
'selection_payload' => ['policy_types' => ['settingsCatalogPolicy']],
'finished_at' => now(),
],
);
$displayName = 'Settings Catalog A';
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
expect($subjectKey)->not->toBeNull();
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('settingsCatalogPolicy', (string) $subjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'settingsCatalogPolicy',
'baseline_hash' => hash('sha256', 'content-a'),
'meta_jsonb' => ['display_name' => $displayName],
]);
// Inventory item exists, but it was NOT observed in the latest sync run.
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'settings-catalog-policy-uuid',
'policy_type' => 'settingsCatalogPolicy',
'display_name' => $displayName,
'meta_jsonb' => ['etag' => 'abc'],
'last_seen_operation_run_id' => (int) $olderInventoryRun->getKey(),
'last_seen_at' => now()->subMinutes(5),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['settingsCatalogPolicy']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run->refresh();
expect($run->status)->toBe('completed');
expect($run->outcome)->toBe('succeeded');
$scopeKey = 'baseline_profile:'.$profile->getKey();
$findings = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->get();
expect($findings->count())->toBe(1);
expect($findings->first()?->evidence_jsonb['change_type'] ?? null)->toBe('missing_policy');
});
it('uses recurrence-key identity (no hashes in fingerprint) and increments times_seen at most once per run', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$builder = app(InventoryMetaContract::class);
$hasher = app(DriftHasher::class);
$baselineContract = $builder->build(
policyType: 'deviceConfiguration',
subjectExternalId: 'policy-x-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE'],
);
$displayName = 'Policy X';
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
expect($subjectKey)->not->toBeNull();
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => $hasher->hashNormalized($baselineContract),
'meta_jsonb' => ['display_name' => $displayName],
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'policy-x-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_1'],
'display_name' => $displayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run1 = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
$job = new CompareBaselineToTenantJob($run1);
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$scopeKey = 'baseline_profile:'.$profile->getKey();
$finding = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->sole();
expect($finding->recurrence_key)->not->toBeNull();
expect($finding->fingerprint)->toBe($finding->recurrence_key);
expect($finding->times_seen)->toBe(1);
$fingerprint = (string) $finding->fingerprint;
$currentHash1 = (string) data_get($finding->evidence_jsonb, 'current.hash');
expect($currentHash1)->not->toBe('');
expect(data_get($finding->evidence_jsonb, 'summary.kind'))->toBe('policy_snapshot');
// Retry the same run ID (job retry): times_seen MUST NOT increment twice for the same run.
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$finding->refresh();
expect($finding->times_seen)->toBe(1);
expect((string) $finding->fingerprint)->toBe($fingerprint);
expect(data_get($finding->evidence_jsonb, 'summary.kind'))->toBe('policy_snapshot');
expect((string) data_get($finding->evidence_jsonb, 'current.hash'))->toBe($currentHash1);
// Change inventory evidence (hash changes) and run compare again with a new OperationRun.
InventoryItem::query()
->where('tenant_id', $tenant->getKey())
->where('policy_type', 'deviceConfiguration')
->where('external_id', 'policy-x-uuid')
->update([
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_2'],
]);
$run2 = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run2))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$finding->refresh();
expect((string) $finding->fingerprint)->toBe($fingerprint);
expect($finding->times_seen)->toBe(2);
expect(data_get($finding->evidence_jsonb, 'summary.kind'))->toBe('policy_snapshot');
expect((string) data_get($finding->evidence_jsonb, 'current.hash'))->not->toBe($currentHash1);
});
it('does not create new finding identities when a new snapshot is captured', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$builder = app(InventoryMetaContract::class);
$hasher = app(DriftHasher::class);
$baselineContract = $builder->build(
policyType: 'deviceConfiguration',
subjectExternalId: 'policy-x-uuid',
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE'],
);
$baselineHash = $hasher->hashNormalized($baselineContract);
$displayName = 'Policy X';
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
expect($subjectKey)->not->toBeNull();
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
$snapshot1 = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot1->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => $baselineHash,
'meta_jsonb' => ['display_name' => $displayName],
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'policy-x-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT'],
'display_name' => $displayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run1 = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot1->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run1))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$scopeKey = 'baseline_profile:'.$profile->getKey();
$finding = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->sole();
expect($finding->times_seen)->toBe(1);
$fingerprint1 = (string) $finding->fingerprint;
$snapshot2 = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot2->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => $baselineHash,
'meta_jsonb' => ['display_name' => $displayName],
]);
$run2 = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot2->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run2))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$findings = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->orderBy('id')
->get();
expect($findings)->toHaveCount(1);
expect((string) $findings->first()?->fingerprint)->toBe($fingerprint1);
expect((int) $findings->first()?->times_seen)->toBe(2);
});
it('creates zero findings when baseline matches tenant inventory exactly', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
// Baseline item
$metaContent = [
'odata_type' => '#microsoft.graph.deviceConfiguration',
'etag' => 'E1',
'scope_tag_ids' => ['scope-2', 'scope-1'],
'assignment_target_count' => 3,
];
$builder = app(InventoryMetaContract::class);
$driftHasher = app(DriftHasher::class);
$contentHash = $driftHasher->hashNormalized($builder->build(
policyType: 'deviceConfiguration',
subjectExternalId: 'matching-uuid',
metaJsonb: $metaContent,
));
$displayName = 'Matching Policy';
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
expect($subjectKey)->not->toBeNull();
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => $contentHash,
'meta_jsonb' => ['display_name' => $displayName],
]);
// Tenant inventory with same content → same hash
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'matching-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => $metaContent,
'display_name' => $displayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
$job = new CompareBaselineToTenantJob($run);
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run->refresh();
expect($run->status)->toBe('completed');
expect($run->outcome)->toBe('succeeded');
$counts = is_array($run->summary_counts) ? $run->summary_counts : [];
expect((int) ($counts['total'] ?? -1))->toBe(0);
$findings = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->count();
expect($findings)->toBe(0);
});
it('does not create missing_policy findings for baseline snapshot items outside effective scope', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$metaContent = [
'odata_type' => '#microsoft.graph.deviceConfiguration',
'etag' => 'E1',
'scope_tag_ids' => ['scope-2', 'scope-1'],
'assignment_target_count' => 3,
];
$builder = app(InventoryMetaContract::class);
$driftHasher = app(DriftHasher::class);
$contentHash = $driftHasher->hashNormalized($builder->build(
policyType: 'deviceConfiguration',
subjectExternalId: 'matching-uuid',
metaJsonb: $metaContent,
));
$displayName = 'Matching Policy';
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
expect($subjectKey)->not->toBeNull();
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $workspaceSafeExternalId,
'subject_key' => (string) $subjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => $contentHash,
'meta_jsonb' => ['display_name' => $displayName],
]);
$foundationDisplayName = 'Foundation Template';
$foundationSubjectKey = BaselineSubjectKey::fromDisplayName($foundationDisplayName);
expect($foundationSubjectKey)->not->toBeNull();
$foundationWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId(
'notificationMessageTemplate',
(string) $foundationSubjectKey,
);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $foundationWorkspaceSafeExternalId,
'subject_key' => (string) $foundationSubjectKey,
'policy_type' => 'notificationMessageTemplate',
'baseline_hash' => hash('sha256', 'foundation-content'),
'meta_jsonb' => ['display_name' => $foundationDisplayName],
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'matching-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => $metaContent,
'display_name' => $displayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'foundation-uuid',
'policy_type' => 'notificationMessageTemplate',
'meta_jsonb' => ['some' => 'value'],
'display_name' => $foundationDisplayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run = $run->fresh();
$counts = is_array($run->summary_counts) ? $run->summary_counts : [];
expect((int) ($counts['total'] ?? -1))->toBe(0);
$scopeKey = 'baseline_profile:'.$profile->getKey();
expect(
Finding::query()
->where('tenant_id', $tenant->getKey())
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey)
->count()
)->toBe(0);
});
// --- T042: Summary counts severity breakdown tests ---
it('writes severity breakdown in summary_counts', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
// 2 baseline items: one will be missing (high), one will be different (medium)
$missingDisplayName = 'Missing Policy';
$missingSubjectKey = BaselineSubjectKey::fromDisplayName($missingDisplayName);
expect($missingSubjectKey)->not->toBeNull();
$missingWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $missingSubjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $missingWorkspaceSafeExternalId,
'subject_key' => (string) $missingSubjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'missing-content'),
'meta_jsonb' => ['display_name' => $missingDisplayName],
]);
$changedDisplayName = 'Changed Policy';
$changedSubjectKey = BaselineSubjectKey::fromDisplayName($changedDisplayName);
expect($changedSubjectKey)->not->toBeNull();
$changedWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $changedSubjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $changedWorkspaceSafeExternalId,
'subject_key' => (string) $changedSubjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'original-content'),
'meta_jsonb' => ['display_name' => $changedDisplayName],
]);
// Tenant only has changed-uuid with different content + extra-uuid (unexpected)
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'changed-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['modified_content' => true],
'display_name' => $changedDisplayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'extra-uuid',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['extra_content' => true],
'display_name' => 'Extra Policy',
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
$job = new CompareBaselineToTenantJob($run);
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run->refresh();
$counts = is_array($run->summary_counts) ? $run->summary_counts : [];
expect((int) ($counts['total'] ?? -1))->toBe(3);
expect((int) ($counts['high'] ?? -1))->toBe(1); // missing-uuid
expect((int) ($counts['medium'] ?? -1))->toBe(1); // changed-uuid
expect((int) ($counts['low'] ?? -1))->toBe(1); // extra-uuid
});
it('writes result context with findings breakdown', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
// One missing policy
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => 'gone-uuid',
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'gone-content'),
]);
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
$job = new CompareBaselineToTenantJob($run);
$job->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$opService,
);
$run->refresh();
$context = is_array($run->context) ? $run->context : [];
$result = $context['result'] ?? [];
expect($result)->toHaveKey('findings_total');
expect($result)->toHaveKey('findings_upserted');
expect($result)->toHaveKey('severity_breakdown');
expect((int) $result['findings_total'])->toBe(1);
expect((int) $result['findings_upserted'])->toBe(1);
});
it('reopens a previously resolved baseline finding when the same drift reappears', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => 'policy-reappears',
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'baseline-content'),
'meta_jsonb' => ['display_name' => 'Policy Reappears'],
]);
$operationRuns = app(OperationRunService::class);
$firstRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($firstRun))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$finding = Finding::query()
->where('tenant_id', (int) $tenant->getKey())
->where('source', 'baseline.compare')
->sole();
$finding->forceFill([
'status' => Finding::STATUS_RESOLVED,
'resolved_at' => now()->subMinute(),
'resolved_reason' => 'manually_resolved',
])->save();
$firstRun->update(['completed_at' => now()->subMinute()]);
$secondRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($secondRun))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$finding->refresh();
expect($finding->status)->toBe(Finding::STATUS_REOPENED);
expect($finding->resolved_at)->toBeNull();
expect($finding->resolved_reason)->toBeNull();
expect($finding->reopened_at)->not->toBeNull();
});
it('preserves an existing open workflow status when the same baseline drift is seen again', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => 'triaged-policy',
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'baseline-content'),
'meta_jsonb' => ['display_name' => 'Triaged Policy'],
]);
$operationRuns = app(OperationRunService::class);
$firstRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($firstRun))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$finding = Finding::query()
->where('tenant_id', (int) $tenant->getKey())
->where('source', 'baseline.compare')
->sole();
$finding->forceFill([
'status' => Finding::STATUS_TRIAGED,
'triaged_at' => now()->subMinute(),
])->save();
$firstRun->update(['completed_at' => now()->subMinute()]);
$secondRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($secondRun))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$finding->refresh();
expect($finding->status)->toBe(Finding::STATUS_TRIAGED);
expect($finding->current_operation_run_id)->toBe((int) $secondRun->getKey());
});
it('applies the workspace baseline severity mapping by change type', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
WorkspaceSetting::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'domain' => 'baseline',
'key' => 'severity_mapping',
'value' => [
'missing_policy' => Finding::SEVERITY_CRITICAL,
'different_version' => Finding::SEVERITY_LOW,
'unexpected_policy' => Finding::SEVERITY_MEDIUM,
],
'updated_by_user_id' => (int) $user->getKey(),
]);
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration']],
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => $tenant->workspace_id,
'baseline_profile_id' => $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => $snapshot->getKey()]);
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
$missingDisplayName = 'Missing Policy';
$missingSubjectKey = BaselineSubjectKey::fromDisplayName($missingDisplayName);
expect($missingSubjectKey)->not->toBeNull();
$missingWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $missingSubjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $missingWorkspaceSafeExternalId,
'subject_key' => (string) $missingSubjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'baseline-a'),
'meta_jsonb' => ['display_name' => $missingDisplayName],
]);
$differentDisplayName = 'Different Policy';
$differentSubjectKey = BaselineSubjectKey::fromDisplayName($differentDisplayName);
expect($differentSubjectKey)->not->toBeNull();
$differentWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $differentSubjectKey);
BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => $snapshot->getKey(),
'subject_type' => 'policy',
'subject_external_id' => $differentWorkspaceSafeExternalId,
'subject_key' => (string) $differentSubjectKey,
'policy_type' => 'deviceConfiguration',
'baseline_hash' => hash('sha256', 'baseline-b'),
'meta_jsonb' => ['display_name' => $differentDisplayName],
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'different-policy',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['different_content' => true],
'display_name' => $differentDisplayName,
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'workspace_id' => $tenant->workspace_id,
'external_id' => 'unexpected-policy',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['unexpected' => true],
'display_name' => 'Unexpected Policy',
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$operationRuns = app(OperationRunService::class);
$run = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration']],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($run))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$findings = Finding::query()
->where('tenant_id', (int) $tenant->getKey())
->where('source', 'baseline.compare')
->get()
->keyBy(fn (Finding $finding): string => (string) data_get($finding->evidence_jsonb, 'change_type'));
expect($findings['missing_policy']->severity)->toBe(Finding::SEVERITY_CRITICAL);
expect($findings['different_version']->severity)->toBe(Finding::SEVERITY_LOW);
expect($findings['unexpected_policy']->severity)->toBe(Finding::SEVERITY_MEDIUM);
});
it('writes numeric-only summary_counts with whitelisted keys for capture and compare runs', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]);
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'external_id' => 'policy-a',
'policy_type' => 'deviceConfiguration',
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E1'],
]);
$operationRuns = app(OperationRunService::class);
$captureRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'source_tenant_id' => (int) $tenant->getKey(),
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CaptureBaselineSnapshotJob($captureRun))->handle(
app(BaselineSnapshotIdentity::class),
app(InventoryMetaContract::class),
app(AuditLogger::class),
$operationRuns,
);
$captureRun->refresh();
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
tenant: $tenant,
statusByType: ['deviceConfiguration' => 'succeeded'],
);
InventoryItem::query()
->where('tenant_id', (int) $tenant->getKey())
->update([
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
'last_seen_at' => now(),
]);
$snapshotId = (int) ($profile->fresh()?->active_snapshot_id ?? 0);
expect($snapshotId)->toBeGreaterThan(0);
$compareRun = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: OperationRunType::BaselineCompare->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => $snapshotId,
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
],
initiator: $user,
);
(new CompareBaselineToTenantJob($compareRun))->handle(
app(BaselineSnapshotIdentity::class),
app(AuditLogger::class),
$operationRuns,
);
$allowedKeys = OperationSummaryKeys::all();
foreach ([$captureRun->fresh(), $compareRun->fresh()] as $run) {
$counts = is_array($run?->summary_counts) ? $run->summary_counts : [];
foreach ($counts as $key => $value) {
expect($allowedKeys)->toContain((string) $key);
expect(is_array($value))->toBeFalse();
expect(is_numeric($value))->toBeTrue();
}
}
});