189 lines
6.1 KiB
PHP
189 lines
6.1 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\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\WorkspaceSetting;
|
|
use App\Services\Baselines\BaselineAutoCloseService;
|
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
|
use App\Services\Drift\DriftHasher;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
|
|
/**
|
|
* @return array{0: User, 1: Tenant, 2: BaselineProfile, 3: BaselineSnapshot}
|
|
*/
|
|
function createBaselineOperabilityFixture(): array
|
|
{
|
|
[$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()]);
|
|
|
|
return [$user, $tenant, $profile, $snapshot];
|
|
}
|
|
|
|
function runBaselineCompareForSnapshot(
|
|
User $user,
|
|
Tenant $tenant,
|
|
BaselineProfile $profile,
|
|
BaselineSnapshot $snapshot,
|
|
): OperationRun {
|
|
$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,
|
|
);
|
|
|
|
$job = new CompareBaselineToTenantJob($run);
|
|
$job->handle(
|
|
app(DriftHasher::class),
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
$operationRuns,
|
|
);
|
|
|
|
return $run->fresh();
|
|
}
|
|
|
|
it('resolves stale baseline findings after a fully successful compare', function (): void {
|
|
[$user, $tenant, $profile, $firstSnapshot] = createBaselineOperabilityFixture();
|
|
|
|
BaselineSnapshotItem::factory()->create([
|
|
'baseline_snapshot_id' => $firstSnapshot->getKey(),
|
|
'subject_type' => 'policy',
|
|
'subject_external_id' => 'stale-policy',
|
|
'policy_type' => 'deviceConfiguration',
|
|
'baseline_hash' => hash('sha256', 'baseline-content'),
|
|
'meta_jsonb' => ['display_name' => 'Stale Policy'],
|
|
]);
|
|
|
|
$firstRun = runBaselineCompareForSnapshot($user, $tenant, $profile, $firstSnapshot);
|
|
|
|
$finding = Finding::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('source', 'baseline.compare')
|
|
->first();
|
|
|
|
expect($finding)->not->toBeNull();
|
|
expect($finding?->status)->toBe(Finding::STATUS_NEW);
|
|
|
|
$secondSnapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => $tenant->workspace_id,
|
|
'baseline_profile_id' => $profile->getKey(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => $secondSnapshot->getKey()]);
|
|
$firstRun->update(['completed_at' => now()->subMinute()]);
|
|
|
|
$secondRun = runBaselineCompareForSnapshot($user, $tenant, $profile, $secondSnapshot);
|
|
|
|
$finding->refresh();
|
|
expect($finding->status)->toBe(Finding::STATUS_RESOLVED);
|
|
expect($finding->resolved_reason)->toBe('no_longer_drifting');
|
|
expect($finding->resolved_at)->not->toBeNull();
|
|
expect($finding->current_operation_run_id)->toBe((int) $secondRun->getKey());
|
|
});
|
|
|
|
dataset('baseline auto close safety gates', [
|
|
'safe and enabled' => [
|
|
OperationRunOutcome::Succeeded->value,
|
|
['total' => 2, 'processed' => 2, 'failed' => 0],
|
|
null,
|
|
true,
|
|
],
|
|
'disabled by workspace setting' => [
|
|
OperationRunOutcome::Succeeded->value,
|
|
['total' => 2, 'processed' => 2, 'failed' => 0],
|
|
false,
|
|
false,
|
|
],
|
|
'partially succeeded outcome' => [
|
|
OperationRunOutcome::PartiallySucceeded->value,
|
|
['total' => 2, 'processed' => 2, 'failed' => 0],
|
|
null,
|
|
false,
|
|
],
|
|
'failed outcome' => [
|
|
OperationRunOutcome::Failed->value,
|
|
['total' => 2, 'processed' => 2, 'failed' => 0],
|
|
null,
|
|
false,
|
|
],
|
|
'incomplete processed count' => [
|
|
OperationRunOutcome::Succeeded->value,
|
|
['total' => 2, 'processed' => 1, 'failed' => 0],
|
|
null,
|
|
false,
|
|
],
|
|
'failed work recorded' => [
|
|
OperationRunOutcome::Succeeded->value,
|
|
['total' => 2, 'processed' => 2, 'failed' => 1],
|
|
null,
|
|
false,
|
|
],
|
|
'missing counters' => [
|
|
OperationRunOutcome::Succeeded->value,
|
|
['processed' => 2, 'failed' => 0],
|
|
null,
|
|
false,
|
|
],
|
|
]);
|
|
|
|
it('gates auto close on outcome completion counts and workspace setting', function (
|
|
string $outcome,
|
|
array $summaryCounts,
|
|
?bool $settingValue,
|
|
bool $expected,
|
|
): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
if ($settingValue !== null) {
|
|
WorkspaceSetting::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'domain' => 'baseline',
|
|
'key' => 'auto_close_enabled',
|
|
'value' => $settingValue,
|
|
'updated_by_user_id' => (int) $user->getKey(),
|
|
]);
|
|
}
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => $outcome,
|
|
'summary_counts' => $summaryCounts,
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
expect(app(BaselineAutoCloseService::class)->shouldAutoClose($tenant, $run))->toBe($expected);
|
|
})->with('baseline auto close safety gates');
|