Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Replaced legacy tenant and environment bindings in the BaselineDriftEngine with the new ProviderResourceIdentity framework as defined in Spec 382.
450 lines
18 KiB
PHP
450 lines
18 KiB
PHP
<?php
|
|
|
|
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\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
|
use App\Services\Baselines\Evidence\ContentEvidenceProvider;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\Baselines\BaselineSubjectKey;
|
|
use App\Support\Baselines\SubjectClass;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunType;
|
|
use Carbon\CarbonImmutable;
|
|
|
|
function rbacRoleDefinitionSnapshot(
|
|
string $displayName,
|
|
string $description,
|
|
bool $isBuiltIn,
|
|
array $allowedActions,
|
|
array $deniedActions = [],
|
|
?string $condition = null,
|
|
array $scopeTagIds = ['0'],
|
|
): array {
|
|
return [
|
|
'displayName' => $displayName,
|
|
'description' => $description,
|
|
'isBuiltIn' => $isBuiltIn,
|
|
'rolePermissions' => [
|
|
[
|
|
'resourceActions' => [
|
|
array_filter([
|
|
'allowedResourceActions' => $allowedActions,
|
|
'notAllowedResourceActions' => $deniedActions,
|
|
'condition' => $condition,
|
|
], static fn (mixed $value): bool => $value !== null),
|
|
],
|
|
],
|
|
],
|
|
'roleScopeTagIds' => $scopeTagIds,
|
|
];
|
|
}
|
|
|
|
function createRoleDefinitionPolicy(ManagedEnvironment $tenant, string $externalId, string $displayName): Policy
|
|
{
|
|
return Policy::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'external_id' => $externalId,
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'platform' => 'all',
|
|
'display_name' => $displayName,
|
|
]);
|
|
}
|
|
|
|
function createRoleDefinitionVersion(Policy $policy, CarbonImmutable $capturedAt, int $versionNumber, array $snapshot): PolicyVersion
|
|
{
|
|
return PolicyVersion::factory()->create([
|
|
'managed_environment_id' => (int) $policy->managed_environment_id,
|
|
'policy_id' => (int) $policy->getKey(),
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'platform' => 'all',
|
|
'version_number' => $versionNumber,
|
|
'captured_at' => $capturedAt,
|
|
'snapshot' => $snapshot,
|
|
'assignments' => [],
|
|
'scope_tags' => [],
|
|
]);
|
|
}
|
|
|
|
function createBaselineRoleDefinitionSnapshotItem(
|
|
BaselineSnapshot $snapshot,
|
|
PolicyVersion $version,
|
|
string $externalId,
|
|
string $displayName,
|
|
bool $isBuiltIn,
|
|
int $rolePermissionCount = 1,
|
|
): BaselineSnapshotItem {
|
|
$subjectKey = baselineProviderResourceSubjectKeyForTest(
|
|
'intuneRoleDefinition',
|
|
$externalId,
|
|
SubjectClass::FoundationBacked,
|
|
);
|
|
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('intuneRoleDefinition', $subjectKey);
|
|
|
|
expect($subjectKey)->not->toBeNull();
|
|
expect($workspaceSafeExternalId)->not->toBeNull();
|
|
|
|
$hash = app(ContentEvidenceProvider::class)->fromPolicyVersion(
|
|
version: $version,
|
|
subjectExternalId: (string) $workspaceSafeExternalId,
|
|
)->hash;
|
|
|
|
return BaselineSnapshotItem::factory()->create([
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'subject_type' => 'policy',
|
|
'subject_external_id' => (string) $workspaceSafeExternalId,
|
|
'subject_key' => (string) $subjectKey,
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'baseline_hash' => $hash,
|
|
'meta_jsonb' => [
|
|
'display_name' => $displayName,
|
|
'category' => 'RBAC',
|
|
'platform' => 'all',
|
|
'evidence' => [
|
|
'fidelity' => 'content',
|
|
'source' => 'policy_version',
|
|
'observed_at' => $version->captured_at?->toIso8601String(),
|
|
],
|
|
'identity' => [
|
|
'strategy' => 'provider_resource',
|
|
'subject_key' => (string) $subjectKey,
|
|
'workspace_subject_external_id' => (string) $workspaceSafeExternalId,
|
|
],
|
|
'version_reference' => [
|
|
'policy_version_id' => (int) $version->getKey(),
|
|
'capture_purpose' => 'baseline_capture',
|
|
],
|
|
'rbac' => [
|
|
'is_built_in' => $isBuiltIn,
|
|
'role_permission_count' => $rolePermissionCount,
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
function createRoleDefinitionInventoryItem(
|
|
ManagedEnvironment $tenant,
|
|
int $inventorySyncRunId,
|
|
string $externalId,
|
|
string $displayName,
|
|
bool $isBuiltIn,
|
|
int $rolePermissionCount = 1,
|
|
): InventoryItem {
|
|
return InventoryItem::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'external_id' => $externalId,
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'display_name' => $displayName,
|
|
'category' => 'RBAC',
|
|
'platform' => 'all',
|
|
'meta_jsonb' => [
|
|
'odata_type' => '#microsoft.graph.deviceAndAppManagementRoleDefinition',
|
|
'is_built_in' => $isBuiltIn,
|
|
'role_permission_count' => $rolePermissionCount,
|
|
],
|
|
'last_seen_operation_run_id' => $inventorySyncRunId,
|
|
'last_seen_at' => now(),
|
|
]);
|
|
}
|
|
|
|
it('classifies intune role definition drift as unchanged modified missing and unexpected with deterministic severity', 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' => ['intuneRoleDefinition'],
|
|
],
|
|
]);
|
|
|
|
$baselineCapturedAt = CarbonImmutable::parse('2026-03-08T10:00:00Z');
|
|
|
|
$snapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => $baselineCapturedAt,
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
|
|
tenant: $tenant,
|
|
statusByType: [
|
|
'deviceConfiguration' => 'succeeded',
|
|
'intuneRoleDefinition' => 'succeeded',
|
|
],
|
|
foundationTypes: ['intuneRoleDefinition'],
|
|
);
|
|
|
|
$stablePolicy = createRoleDefinitionPolicy($tenant, 'role-stable', 'Stable Role');
|
|
$stableBaselineVersion = createRoleDefinitionVersion(
|
|
policy: $stablePolicy,
|
|
capturedAt: $baselineCapturedAt,
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Stable Role', 'Baseline stable role', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createRoleDefinitionVersion(
|
|
policy: $stablePolicy,
|
|
capturedAt: $baselineCapturedAt->addMinutes(10),
|
|
versionNumber: 2,
|
|
snapshot: rbacRoleDefinitionSnapshot('Stable Role', 'Baseline stable role', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createBaselineRoleDefinitionSnapshotItem($snapshot, $stableBaselineVersion, 'role-stable', 'Stable Role', false);
|
|
createRoleDefinitionInventoryItem($tenant, (int) $inventorySyncRun->getKey(), 'role-stable', 'Stable Role', false);
|
|
|
|
$metadataPolicy = createRoleDefinitionPolicy($tenant, 'role-meta', 'Metadata Role');
|
|
$metadataBaselineVersion = createRoleDefinitionVersion(
|
|
policy: $metadataPolicy,
|
|
capturedAt: $baselineCapturedAt,
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Metadata Role', 'Baseline description', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createRoleDefinitionVersion(
|
|
policy: $metadataPolicy,
|
|
capturedAt: $baselineCapturedAt->addMinutes(12),
|
|
versionNumber: 2,
|
|
snapshot: rbacRoleDefinitionSnapshot('Metadata Role', 'Updated description', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createBaselineRoleDefinitionSnapshotItem($snapshot, $metadataBaselineVersion, 'role-meta', 'Metadata Role', false);
|
|
createRoleDefinitionInventoryItem($tenant, (int) $inventorySyncRun->getKey(), 'role-meta', 'Metadata Role', false);
|
|
|
|
$permissionsPolicy = createRoleDefinitionPolicy($tenant, 'role-permissions', 'Permission Role');
|
|
$permissionsBaselineVersion = createRoleDefinitionVersion(
|
|
policy: $permissionsPolicy,
|
|
capturedAt: $baselineCapturedAt,
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Permission Role', 'Baseline permissions', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createRoleDefinitionVersion(
|
|
policy: $permissionsPolicy,
|
|
capturedAt: $baselineCapturedAt->addMinutes(14),
|
|
versionNumber: 2,
|
|
snapshot: rbacRoleDefinitionSnapshot('Permission Role', 'Baseline permissions', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
'Microsoft.Intune/deviceConfigurations/delete',
|
|
]),
|
|
);
|
|
createBaselineRoleDefinitionSnapshotItem($snapshot, $permissionsBaselineVersion, 'role-permissions', 'Permission Role', false);
|
|
createRoleDefinitionInventoryItem($tenant, (int) $inventorySyncRun->getKey(), 'role-permissions', 'Permission Role', false);
|
|
|
|
$missingPolicy = createRoleDefinitionPolicy($tenant, 'role-missing', 'Missing Role');
|
|
$missingBaselineVersion = createRoleDefinitionVersion(
|
|
policy: $missingPolicy,
|
|
capturedAt: $baselineCapturedAt,
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Missing Role', 'Baseline missing role', false, [
|
|
'Microsoft.Intune/deviceCompliancePolicies/read',
|
|
]),
|
|
);
|
|
createBaselineRoleDefinitionSnapshotItem($snapshot, $missingBaselineVersion, 'role-missing', 'Missing Role', false);
|
|
|
|
$unexpectedPolicy = createRoleDefinitionPolicy($tenant, 'role-unexpected', 'Unexpected Role');
|
|
createRoleDefinitionVersion(
|
|
policy: $unexpectedPolicy,
|
|
capturedAt: $baselineCapturedAt->addMinutes(16),
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Unexpected Role', 'Unexpected current role', true, [
|
|
'Microsoft.Intune/deviceCompliancePolicies/read',
|
|
]),
|
|
);
|
|
createRoleDefinitionInventoryItem($tenant, (int) $inventorySyncRun->getKey(), 'role-unexpected', 'Unexpected Role', true);
|
|
|
|
InventoryItem::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'external_id' => 'assignment-noise',
|
|
'policy_type' => 'intuneRoleAssignment',
|
|
'display_name' => 'Assignment Noise',
|
|
'category' => 'RBAC',
|
|
'platform' => 'all',
|
|
'meta_jsonb' => [
|
|
'odata_type' => '#microsoft.graph.deviceAndAppManagementRoleAssignment',
|
|
],
|
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
$operationRuns = app(OperationRunService::class);
|
|
$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' => (int) $snapshot->getKey(),
|
|
'effective_scope' => [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => ['intuneRoleDefinition'],
|
|
],
|
|
],
|
|
initiator: $user,
|
|
);
|
|
|
|
(new CompareBaselineToTenantJob($compareRun))->handle(
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
$operationRuns,
|
|
);
|
|
|
|
$compareRun->refresh();
|
|
|
|
expect($compareRun->outcome)->toBe(OperationRunOutcome::Succeeded->value);
|
|
expect(data_get($compareRun->context, 'baseline_compare.reason_code'))->toBeNull();
|
|
expect(data_get($compareRun->context, 'baseline_compare.rbac_role_definitions'))->toBe([
|
|
'total_compared' => 5,
|
|
'unchanged' => 1,
|
|
'modified' => 2,
|
|
'missing' => 1,
|
|
'unexpected' => 1,
|
|
]);
|
|
expect(data_get($compareRun->context, 'findings.counts_by_change_type'))->toBe([
|
|
'different_version' => 2,
|
|
'missing_policy' => 1,
|
|
'unexpected_policy' => 1,
|
|
]);
|
|
|
|
$findings = Finding::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('source', 'baseline.compare')
|
|
->get()
|
|
->keyBy(fn (Finding $finding): string => (string) data_get($finding->evidence_jsonb, 'display_name'));
|
|
|
|
expect($findings)->toHaveCount(4);
|
|
expect($findings->has('Stable Role'))->toBeFalse();
|
|
expect($findings->has('Metadata Role'))->toBeTrue();
|
|
expect($findings->has('Permission Role'))->toBeTrue();
|
|
expect($findings->has('Missing Role'))->toBeTrue();
|
|
expect($findings->has('Unexpected Role'))->toBeTrue();
|
|
|
|
expect($findings['Metadata Role']->severity)->toBe(Finding::SEVERITY_LOW);
|
|
expect(data_get($findings['Metadata Role']->evidence_jsonb, 'change_type'))->toBe('different_version');
|
|
|
|
expect($findings['Permission Role']->severity)->toBe(Finding::SEVERITY_HIGH);
|
|
expect(data_get($findings['Permission Role']->evidence_jsonb, 'change_type'))->toBe('different_version');
|
|
|
|
expect($findings['Missing Role']->severity)->toBe(Finding::SEVERITY_HIGH);
|
|
expect(data_get($findings['Missing Role']->evidence_jsonb, 'change_type'))->toBe('missing_policy');
|
|
|
|
expect($findings['Unexpected Role']->severity)->toBe(Finding::SEVERITY_MEDIUM);
|
|
expect(data_get($findings['Unexpected Role']->evidence_jsonb, 'change_type'))->toBe('unexpected_policy');
|
|
});
|
|
|
|
it('treats a recreated same-name role definition with a new id as missing plus unexpected drift', 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' => ['intuneRoleDefinition'],
|
|
],
|
|
]);
|
|
|
|
$baselineCapturedAt = CarbonImmutable::parse('2026-03-08T11:00:00Z');
|
|
|
|
$snapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => $baselineCapturedAt,
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
$inventorySyncRun = createInventorySyncOperationRunWithCoverage(
|
|
tenant: $tenant,
|
|
statusByType: [
|
|
'deviceConfiguration' => 'succeeded',
|
|
'intuneRoleDefinition' => 'succeeded',
|
|
],
|
|
foundationTypes: ['intuneRoleDefinition'],
|
|
);
|
|
|
|
$baselinePolicy = createRoleDefinitionPolicy($tenant, 'role-old-id', 'Security Reader');
|
|
$baselineVersion = createRoleDefinitionVersion(
|
|
policy: $baselinePolicy,
|
|
capturedAt: $baselineCapturedAt,
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Security Reader', 'Baseline role definition', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createBaselineRoleDefinitionSnapshotItem($snapshot, $baselineVersion, 'role-old-id', 'Security Reader', false);
|
|
|
|
$currentPolicy = createRoleDefinitionPolicy($tenant, 'role-new-id', 'Security Reader');
|
|
createRoleDefinitionVersion(
|
|
policy: $currentPolicy,
|
|
capturedAt: $baselineCapturedAt->addMinutes(5),
|
|
versionNumber: 1,
|
|
snapshot: rbacRoleDefinitionSnapshot('Security Reader', 'Recreated role definition', false, [
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]),
|
|
);
|
|
createRoleDefinitionInventoryItem($tenant, (int) $inventorySyncRun->getKey(), 'role-new-id', 'Security Reader', false);
|
|
|
|
$operationRuns = app(OperationRunService::class);
|
|
$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' => (int) $snapshot->getKey(),
|
|
'effective_scope' => [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => ['intuneRoleDefinition'],
|
|
],
|
|
],
|
|
initiator: $user,
|
|
);
|
|
|
|
(new CompareBaselineToTenantJob($compareRun))->handle(
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
$operationRuns,
|
|
);
|
|
|
|
$compareRun->refresh();
|
|
|
|
expect(data_get($compareRun->context, 'baseline_compare.rbac_role_definitions'))->toBe([
|
|
'total_compared' => 2,
|
|
'unchanged' => 0,
|
|
'modified' => 0,
|
|
'missing' => 1,
|
|
'unexpected' => 1,
|
|
]);
|
|
|
|
$findings = Finding::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('source', 'baseline.compare')
|
|
->get();
|
|
|
|
expect($findings)->toHaveCount(2);
|
|
expect($findings->pluck('severity')->sort()->values()->all())->toBe([
|
|
Finding::SEVERITY_HIGH,
|
|
Finding::SEVERITY_MEDIUM,
|
|
]);
|
|
expect($findings->pluck('evidence_jsonb.change_type')->sort()->values()->all())->toBe([
|
|
'missing_policy',
|
|
'unexpected_policy',
|
|
]);
|
|
});
|