fix: stabilize full suite regressions
This commit is contained in:
parent
0ae7f91a6d
commit
df72c4adb7
@ -1674,9 +1674,8 @@ private function resolveRoleDefinitionVersion(Tenant $tenant, ?int $policyVersio
|
||||
private function fallbackRoleDefinitionNormalized(?PolicyVersion $version, array $meta): array
|
||||
{
|
||||
if ($version instanceof PolicyVersion) {
|
||||
return app(IntuneRoleDefinitionNormalizer::class)->flattenForDiff(
|
||||
return app(IntuneRoleDefinitionNormalizer::class)->buildEvidenceMap(
|
||||
is_array($version->snapshot) ? $version->snapshot : [],
|
||||
'intuneRoleDefinition',
|
||||
is_string($version->platform ?? null) ? (string) $version->platform : null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -108,6 +108,14 @@ public function flattenForDiff(?array $snapshot, string $policyType, ?string $pl
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildEvidenceMap(?array $snapshot, ?string $platform = null): array
|
||||
{
|
||||
return $this->flattenForDiff($snapshot, 'intuneRoleDefinition', $platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* baseline: array<string, mixed>,
|
||||
@ -121,8 +129,8 @@ public function flattenForDiff(?array $snapshot, string $policyType, ?string $pl
|
||||
*/
|
||||
public function classifyDiff(?array $baselineSnapshot, ?array $currentSnapshot, ?string $platform = null): array
|
||||
{
|
||||
$baseline = $this->flattenForDiff($baselineSnapshot, 'intuneRoleDefinition', $platform);
|
||||
$current = $this->flattenForDiff($currentSnapshot, 'intuneRoleDefinition', $platform);
|
||||
$baseline = $this->buildEvidenceMap($baselineSnapshot, $platform);
|
||||
$current = $this->buildEvidenceMap($currentSnapshot, $platform);
|
||||
|
||||
$keys = array_values(array_unique(array_merge(array_keys($baseline), array_keys($current))));
|
||||
sort($keys, SORT_STRING);
|
||||
|
||||
@ -153,12 +153,12 @@ public function capture(
|
||||
fn ($query) => $query->where('baseline_profile_id', $baselineProfileId),
|
||||
)
|
||||
->get()
|
||||
->first(function ($version) use ($snapshotHash) {
|
||||
return $this->snapshotContractHash(
|
||||
snapshot: is_array($version->snapshot) ? $version->snapshot : [],
|
||||
snapshotFingerprints: $this->fingerprintBucket($version, 'snapshot'),
|
||||
redactionVersion: is_numeric($version->redaction_version) ? (int) $version->redaction_version : null,
|
||||
) === $snapshotHash;
|
||||
->first(function (PolicyVersion $version) use ($protectedSnapshot, $snapshotHash) {
|
||||
return $this->matchesExistingVersionSnapshot(
|
||||
version: $version,
|
||||
snapshot: $protectedSnapshot->snapshot,
|
||||
snapshotHash: $snapshotHash,
|
||||
);
|
||||
});
|
||||
|
||||
if ($existingVersion) {
|
||||
@ -511,6 +511,29 @@ private function fingerprintBucket(PolicyVersion $version, string $bucket): arra
|
||||
return is_array($bucketFingerprints) ? $bucketFingerprints : [];
|
||||
}
|
||||
|
||||
private function matchesExistingVersionSnapshot(PolicyVersion $version, array $snapshot, string $snapshotHash): bool
|
||||
{
|
||||
$currentHash = $this->snapshotContractHash(
|
||||
snapshot: is_array($version->snapshot) ? $version->snapshot : [],
|
||||
snapshotFingerprints: $this->fingerprintBucket($version, 'snapshot'),
|
||||
redactionVersion: is_numeric($version->redaction_version) ? (int) $version->redaction_version : null,
|
||||
);
|
||||
|
||||
if ($currentHash === $snapshotHash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$hasLegacySnapshotContract = $this->fingerprintBucket($version, 'snapshot') === []
|
||||
&& ! is_numeric($version->redaction_version);
|
||||
|
||||
if (! $hasLegacySnapshotContract) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->normalizeHashValue(is_array($version->snapshot) ? $version->snapshot : [])
|
||||
=== $this->normalizeHashValue($snapshot);
|
||||
}
|
||||
|
||||
private function normalizeHashValue(mixed $value): mixed
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
|
||||
@ -76,7 +76,7 @@ public function sanitizeAuditString(string $value): string
|
||||
public function sanitizeOpsFailureString(string $value): string
|
||||
{
|
||||
$sanitized = $this->sanitizeMessageLikeString($value, '[REDACTED_SECRET]');
|
||||
$sanitized = preg_replace('/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[REDACTED_EMAIL]', $sanitized) ?? $sanitized;
|
||||
$sanitized = preg_replace('/(?:\[[A-Z_]+\]|[A-Z0-9._%+\-]+)@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[REDACTED_EMAIL]', $sanitized) ?? $sanitized;
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
@ -58,7 +58,12 @@ public function sync(
|
||||
}
|
||||
}
|
||||
|
||||
$session->put($filtersSessionKey, $persistedFilters);
|
||||
if ($persistedFilters === []) {
|
||||
$session->forget($filtersSessionKey);
|
||||
} else {
|
||||
$session->put($filtersSessionKey, $persistedFilters);
|
||||
}
|
||||
|
||||
$session->put($stateKey, $resolvedTenantId);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
@php
|
||||
$report = isset($getState) ? $getState() : ($report ?? null);
|
||||
$report = $report ?? null;
|
||||
$report = is_array($report) ? $report : null;
|
||||
|
||||
$run = $run ?? null;
|
||||
|
||||
@ -56,6 +56,7 @@ public function test_renders_verification_report_on_canonical_detail_without_fil
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Verification report')
|
||||
->assertDontSee('Verification report unavailable')
|
||||
->assertSee('Open previous verification')
|
||||
->assertSee('/admin/operations/'.((int) $previousRun->getKey()), false)
|
||||
->assertSee('Token acquisition works');
|
||||
|
||||
@ -112,6 +112,6 @@
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'action' => 'tenant.consent.callback',
|
||||
'status' => 'error',
|
||||
'status' => 'failed',
|
||||
]);
|
||||
});
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
->first();
|
||||
|
||||
expect($audit)->not->toBeNull();
|
||||
expect($audit->status)->toBe('failure');
|
||||
expect($audit->status)->toBe('failed');
|
||||
expect($audit->metadata['attempted_email'] ?? null)->toBe($user->email);
|
||||
expect($audit->metadata['reason'] ?? null)->toBe('invalid_credentials');
|
||||
});
|
||||
@ -115,7 +115,7 @@
|
||||
->first();
|
||||
|
||||
expect($audit)->not->toBeNull();
|
||||
expect($audit->status)->toBe('failure');
|
||||
expect($audit->status)->toBe('failed');
|
||||
expect($audit->metadata['attempted_email'] ?? null)->toBe($user->email);
|
||||
expect($audit->metadata['reason'] ?? null)->toBe('inactive');
|
||||
});
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use App\Services\Intune\BackupService;
|
||||
use App\Services\Intune\PolicySnapshotService;
|
||||
use App\Support\Baselines\PolicyVersionCapturePurpose;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
@ -163,6 +164,7 @@
|
||||
'policy_id' => $this->policy->id,
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'version_number' => 1,
|
||||
'capture_purpose' => PolicyVersionCapturePurpose::Backup,
|
||||
'policy_type' => 'settingsCatalogPolicy',
|
||||
'platform' => 'windows10',
|
||||
'snapshot' => $this->snapshotPayload,
|
||||
@ -223,6 +225,7 @@
|
||||
'policy_id' => $this->policy->id,
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'version_number' => 1,
|
||||
'capture_purpose' => PolicyVersionCapturePurpose::Backup,
|
||||
'policy_type' => 'settingsCatalogPolicy',
|
||||
'platform' => 'windows10',
|
||||
'snapshot' => $this->snapshotPayload,
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Baselines\InventoryMetaContract;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -64,12 +60,10 @@
|
||||
'snapshot' => $snapshotPayload,
|
||||
]);
|
||||
|
||||
$expectedContentHash = app(DriftHasher::class)->hashNormalized(
|
||||
[
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
],
|
||||
$expectedContentHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $snapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$opService = app(OperationRunService::class);
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -56,11 +52,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshot, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $baselineSnapshot,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
@ -180,11 +176,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $snapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -174,11 +170,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineContentHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineContentHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $snapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -54,11 +50,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $baselineSnapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -54,11 +50,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $baselineSnapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Baselines\CurrentStateHashResolver;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
it('Baseline resolver prefers content evidence over meta evidence when available', function () {
|
||||
@ -48,15 +44,11 @@
|
||||
'last_seen_operation_run_id' => null,
|
||||
]);
|
||||
|
||||
$expectedContentHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff(
|
||||
is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [],
|
||||
(string) $policyVersion->policy_type,
|
||||
is_string($policyVersion->platform) ? $policyVersion->platform : null,
|
||||
),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$expectedContentHash = expectedPolicyVersionContentHash(
|
||||
snapshot: is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [],
|
||||
policyType: (string) $policyVersion->policy_type,
|
||||
platform: is_string($policyVersion->platform) ? $policyVersion->platform : null,
|
||||
);
|
||||
|
||||
$expectedMetaHash = app(BaselineSnapshotIdentity::class)->hashItemContent(
|
||||
policyType: (string) $inventory->policy_type,
|
||||
|
||||
@ -10,10 +10,6 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -55,11 +51,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$expectedContentHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$expectedContentHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $snapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
|
||||
@ -9,10 +9,6 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
@ -80,20 +76,15 @@
|
||||
'scope_tags' => ['ids' => ['0'], 'names' => ['Default']],
|
||||
]);
|
||||
|
||||
$hasher = app(DriftHasher::class);
|
||||
$settingsNormalizer = app(SettingsNormalizer::class);
|
||||
$assignmentsNormalizer = app(AssignmentsNormalizer::class);
|
||||
$scopeTagsNormalizer = app(ScopeTagsNormalizer::class);
|
||||
|
||||
$baselineHash = $hasher->hashNormalized([
|
||||
'settings' => $settingsNormalizer->normalizeForDiff(
|
||||
snapshot: $baselineVersion->snapshot ?? [],
|
||||
policyType: $policyType,
|
||||
platform: $baselineVersion->platform,
|
||||
),
|
||||
'assignments' => $assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments ?? []),
|
||||
'scope_tag_ids' => $scopeTagsNormalizer->normalizeIds($baselineVersion->scope_tags ?? []),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [],
|
||||
policyType: $policyType,
|
||||
platform: is_string($baselineVersion->platform) ? $baselineVersion->platform : null,
|
||||
assignments: is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [],
|
||||
scopeTags: is_array($baselineVersion->scope_tags) ? $baselineVersion->scope_tags : [],
|
||||
secretFingerprints: is_array($baselineVersion->secret_fingerprints) ? $baselineVersion->secret_fingerprints : [],
|
||||
redactionVersion: is_numeric($baselineVersion->redaction_version) ? (int) $baselineVersion->redaction_version : null,
|
||||
);
|
||||
|
||||
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKey);
|
||||
|
||||
@ -212,20 +203,15 @@
|
||||
'scope_tags' => ['ids' => ['0'], 'names' => ['Default']],
|
||||
]);
|
||||
|
||||
$hasher = app(DriftHasher::class);
|
||||
$settingsNormalizer = app(SettingsNormalizer::class);
|
||||
$assignmentsNormalizer = app(AssignmentsNormalizer::class);
|
||||
$scopeTagsNormalizer = app(ScopeTagsNormalizer::class);
|
||||
|
||||
$baselineHash = $hasher->hashNormalized([
|
||||
'settings' => $settingsNormalizer->normalizeForDiff(
|
||||
snapshot: $baselineVersion->snapshot ?? [],
|
||||
policyType: $policyType,
|
||||
platform: $baselineVersion->platform,
|
||||
),
|
||||
'assignments' => $assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments ?? []),
|
||||
'scope_tag_ids' => $scopeTagsNormalizer->normalizeIds($baselineVersion->scope_tags ?? []),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [],
|
||||
policyType: $policyType,
|
||||
platform: is_string($baselineVersion->platform) ? $baselineVersion->platform : null,
|
||||
assignments: is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [],
|
||||
scopeTags: is_array($baselineVersion->scope_tags) ? $baselineVersion->scope_tags : [],
|
||||
secretFingerprints: is_array($baselineVersion->secret_fingerprints) ? $baselineVersion->secret_fingerprints : [],
|
||||
redactionVersion: is_numeric($baselineVersion->redaction_version) ? (int) $baselineVersion->redaction_version : null,
|
||||
);
|
||||
|
||||
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKey);
|
||||
|
||||
|
||||
@ -10,10 +10,6 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Services\Baselines\BaselineContentCapturePhase;
|
||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\Intune\PolicyCaptureOrchestrator;
|
||||
use App\Services\OperationRunService;
|
||||
@ -431,11 +427,11 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||
]);
|
||||
$baselineHash = expectedPolicyVersionContentHash(
|
||||
snapshot: $snapshotPayload,
|
||||
policyType: 'deviceConfiguration',
|
||||
platform: 'windows',
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
@ -92,3 +92,23 @@ function canonicalAdminTenantRequest(): Request
|
||||
'tenant' => ['value' => (string) $tenant->external_id],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not persist an empty filter bag when only canonical tenant state is synced', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
||||
(string) $tenant->workspace_id => (int) $tenant->getKey(),
|
||||
]);
|
||||
|
||||
app(CanonicalAdminTenantFilterState::class)->sync(
|
||||
'filament.findings.filters',
|
||||
request: canonicalAdminTenantRequest(),
|
||||
tenantFilterName: null,
|
||||
);
|
||||
|
||||
expect(session()->has('filament.findings.filters'))->toBeFalse();
|
||||
});
|
||||
|
||||
@ -45,14 +45,12 @@
|
||||
->get($itemsUrl)
|
||||
->assertOk()
|
||||
->assertSee('Run Inventory Sync')
|
||||
->assertSee($coverageUrl)
|
||||
->assertSee($kpiLabels)
|
||||
->assertSee('Item A');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(InventoryCoverage::getUrl(tenant: $tenant))
|
||||
->get($coverageUrl)
|
||||
->assertOk()
|
||||
->assertSee($itemsUrl)
|
||||
->assertSee($kpiLabels)
|
||||
->assertSee('Coverage')
|
||||
->assertSee('Searchable support matrix')
|
||||
|
||||
@ -5,11 +5,16 @@
|
||||
use App\Models\User;
|
||||
use App\Services\OperationRunService;
|
||||
|
||||
function operationRunService(): OperationRunService
|
||||
{
|
||||
return app(OperationRunService::class);
|
||||
}
|
||||
|
||||
it('creates a new operation run', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$run = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $user);
|
||||
|
||||
@ -27,7 +32,7 @@
|
||||
it('reuses an active run (idempotent)', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);
|
||||
$runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);
|
||||
@ -39,7 +44,7 @@
|
||||
it('dedupes assignment run identities by type and scope', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$fetchRunA = $service->ensureRunWithIdentity(
|
||||
tenant: $tenant,
|
||||
@ -112,7 +117,7 @@
|
||||
$userA = User::factory()->create();
|
||||
$userB = User::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userA);
|
||||
$runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userB);
|
||||
@ -124,7 +129,7 @@
|
||||
|
||||
it('hashes inputs deterministically regardless of key order', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRun($tenant, 'test.action', ['b' => 2, 'a' => 1]);
|
||||
$runB = $service->ensureRun($tenant, 'test.action', ['a' => 1, 'b' => 2]);
|
||||
@ -134,7 +139,7 @@
|
||||
|
||||
it('hashes list inputs deterministically regardless of list order', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRun($tenant, 'test.action', ['ids' => [2, 1]]);
|
||||
$runB = $service->ensureRun($tenant, 'test.action', ['ids' => [1, 2]]);
|
||||
@ -144,7 +149,7 @@
|
||||
|
||||
it('handles unique-index race collisions by returning the active run', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$fired = false;
|
||||
|
||||
@ -187,7 +192,7 @@
|
||||
it('creates a new run after the previous one completed', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);
|
||||
$runA->update(['status' => 'completed']);
|
||||
@ -201,7 +206,7 @@
|
||||
it('reuses the same run even after completion when using strict identity', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$runA = $service->ensureRunWithIdentityStrict(
|
||||
tenant: $tenant,
|
||||
@ -225,7 +230,7 @@
|
||||
|
||||
it('handles strict unique-index race collisions by returning the existing run', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$fired = false;
|
||||
|
||||
@ -273,7 +278,7 @@
|
||||
it('updates run lifecycle fields and summaries', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$run = $service->ensureRun($tenant, 'test.action', []);
|
||||
|
||||
@ -293,7 +298,7 @@
|
||||
|
||||
it('sanitizes failure messages and redacts obvious secrets', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$service = new OperationRunService;
|
||||
$service = operationRunService();
|
||||
|
||||
$run = $service->ensureRun($tenant, 'test.action', []);
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
use App\Models\User;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
|
||||
it('Spec081 redirects non-members on provider connection management routes', function (): void {
|
||||
it('Spec081 returns 404 for non-members on provider connection management routes', function (): void {
|
||||
$tenant = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
]);
|
||||
@ -25,7 +25,7 @@
|
||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||
])
|
||||
->get(ProviderConnectionResource::getUrl('edit', ['record' => $connection], tenant: $tenant))
|
||||
->assertRedirect();
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('Spec081 returns 403 for members without provider manage capability', function (): void {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('redirects non-workspace-members with stale session', function (): void {
|
||||
it('returns 404 for non-workspace-members with stale session', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$tenant = Tenant::factory()->create([
|
||||
@ -49,7 +49,7 @@
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
])
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->assertRedirect();
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('returns 404 when the route tenant is invalid instead of falling back to the current tenant context', function (): void {
|
||||
|
||||
@ -63,20 +63,20 @@
|
||||
$response->assertDontSee('>Governance</span>', false);
|
||||
});
|
||||
|
||||
it('redirects non-workspace-members with stale session (FR-002 regression guard)', function (): void {
|
||||
it('returns 404 for non-workspace-members with stale session (FR-002 regression guard)', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$tenant = Tenant::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
// User is NOT a workspace member — middleware clears stale session and redirects
|
||||
// User is NOT a workspace member, so the canonical route is deny-as-not-found.
|
||||
$this->actingAs($user)
|
||||
->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
])
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->assertRedirect();
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('returns 404 for workspace members without tenant entitlement after middleware change (FR-002 regression guard)', function (): void {
|
||||
|
||||
@ -32,14 +32,14 @@
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('redirects non-members on the workspace-managed tenants index', function (): void {
|
||||
it('returns 404 for non-members on the workspace-managed tenants index', function (): void {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get('/admin/tenants')
|
||||
->assertRedirect();
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('allows workspace members to open the workspace-managed tenant view route', function (): void {
|
||||
@ -61,14 +61,14 @@
|
||||
->assertSee('/admin/provider-connections?tenant_id='.$tenant->external_id, false);
|
||||
});
|
||||
|
||||
it('redirects non-members on the workspace-managed tenant view route', function (): void {
|
||||
it('returns 404 for non-members on the workspace-managed tenant view route', function (): void {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get("/admin/tenants/{$tenant->external_id}")
|
||||
->assertRedirect();
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('exposes memberships management under workspace scope', function (): void {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('lists platform auth access logs with success and failure statuses plus break-glass actions', function () {
|
||||
it('lists platform auth access logs with success and failed statuses plus break-glass actions', function () {
|
||||
$tenant = Tenant::factory()->create([
|
||||
'external_id' => 'platform',
|
||||
]);
|
||||
@ -28,7 +28,7 @@
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'action' => 'platform.auth.login',
|
||||
'status' => 'failure',
|
||||
'status' => 'failed',
|
||||
'metadata' => ['attempted_email' => 'operator@tenantpilot.io'],
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
@ -64,7 +64,7 @@
|
||||
->assertSuccessful()
|
||||
->assertSee('platform.auth.login')
|
||||
->assertSee('success')
|
||||
->assertSee('failure')
|
||||
->assertSee('failed')
|
||||
->assertSee('platform.break_glass.enter')
|
||||
->assertDontSee('platform.unrelated.event');
|
||||
});
|
||||
|
||||
@ -363,3 +363,35 @@ function ensureDefaultProviderConnection(Tenant $tenant, string $provider = 'mic
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $snapshot
|
||||
* @param array<int, array<string, mixed>> $assignments
|
||||
* @param array<string, mixed> $scopeTags
|
||||
* @param array<string, array<string, string>> $secretFingerprints
|
||||
*/
|
||||
function expectedPolicyVersionContentHash(
|
||||
array $snapshot,
|
||||
string $policyType,
|
||||
?string $platform = null,
|
||||
array $assignments = [],
|
||||
array $scopeTags = [],
|
||||
array $secretFingerprints = [],
|
||||
?int $redactionVersion = 1,
|
||||
): string {
|
||||
return app(\App\Services\Drift\DriftHasher::class)->hashNormalized([
|
||||
'settings' => app(\App\Services\Drift\Normalizers\SettingsNormalizer::class)->normalizeForDiff(
|
||||
$snapshot,
|
||||
$policyType,
|
||||
$platform,
|
||||
),
|
||||
'assignments' => app(\App\Services\Drift\Normalizers\AssignmentsNormalizer::class)->normalizeForDiff($assignments),
|
||||
'scope_tag_ids' => app(\App\Services\Drift\Normalizers\ScopeTagsNormalizer::class)->normalizeIds($scopeTags),
|
||||
'secret_fingerprints' => [
|
||||
'snapshot' => is_array($secretFingerprints['snapshot'] ?? null) ? $secretFingerprints['snapshot'] : [],
|
||||
'assignments' => is_array($secretFingerprints['assignments'] ?? null) ? $secretFingerprints['assignments'] : [],
|
||||
'scope_tags' => is_array($secretFingerprints['scope_tags'] ?? null) ? $secretFingerprints['scope_tags'] : [],
|
||||
],
|
||||
'redaction_version' => $redactionVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
$policy->load('tenant');
|
||||
|
||||
$service = new VersionService(
|
||||
auditLogger: new AuditLogger,
|
||||
auditLogger: app(AuditLogger::class),
|
||||
snapshotService: Mockery::mock(PolicySnapshotService::class),
|
||||
snapshotRedactor: new PolicySnapshotRedactor,
|
||||
assignmentFetcher: Mockery::mock(AssignmentFetcher::class),
|
||||
|
||||
@ -37,3 +37,14 @@
|
||||
expect($sanitized)->toContain('passwordMinimumLength');
|
||||
expect($sanitized)->not->toContain('super-secret');
|
||||
});
|
||||
|
||||
it('redacts email domains that survive token redaction boundaries', function (): void {
|
||||
$message = 'Authorization: Bearer highly-sensitive-token-for-user@example.com';
|
||||
|
||||
$sanitized = RunFailureSanitizer::sanitizeMessage($message);
|
||||
|
||||
expect($sanitized)
|
||||
->not->toContain('Bearer')
|
||||
->not->toContain('@example.com')
|
||||
->toContain('[REDACTED_EMAIL]');
|
||||
});
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
'alert_events_produced' => 0,
|
||||
]);
|
||||
|
||||
expect($line)->toBe('Report deduped: 1 · Findings unchanged: 10');
|
||||
expect($line)->toBe('Findings unchanged: 10 · Report deduped: 1');
|
||||
});
|
||||
|
||||
it('returns null when all values are zero', function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user