Spec 118: Resumable baseline evidence capture + snapshot UX #143
@ -72,6 +72,11 @@ class BaselineCompareLanding extends Page
|
||||
|
||||
public ?string $fidelity = null;
|
||||
|
||||
public ?int $evidenceGapsCount = null;
|
||||
|
||||
/** @var array<string, int>|null */
|
||||
public ?array $evidenceGapsTopReasons = null;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
@ -116,6 +121,9 @@ public function refreshStats(): void
|
||||
$this->uncoveredTypesCount = $stats->uncoveredTypesCount;
|
||||
$this->uncoveredTypes = $stats->uncoveredTypes !== [] ? $stats->uncoveredTypes : null;
|
||||
$this->fidelity = $stats->fidelity;
|
||||
|
||||
$this->evidenceGapsCount = $stats->evidenceGapsCount;
|
||||
$this->evidenceGapsTopReasons = $stats->evidenceGapsTopReasons !== [] ? $stats->evidenceGapsTopReasons : null;
|
||||
}
|
||||
|
||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
|
||||
@ -144,7 +144,20 @@ public static function infolist(Schema $schema): Schema
|
||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
||||
TextEntry::make('fingerprint')->label('Fingerprint')->copyable(),
|
||||
TextEntry::make('scope_key')->label('Scope')->copyable(),
|
||||
TextEntry::make('subject_display_name')->label('Subject')->placeholder('—'),
|
||||
TextEntry::make('subject_display_name')
|
||||
->label('Subject')
|
||||
->placeholder('—')
|
||||
->state(function (Finding $record): ?string {
|
||||
$state = $record->subject_display_name;
|
||||
if (is_string($state) && trim($state) !== '') {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$fallback = Arr::get($record->evidence_jsonb ?? [], 'display_name');
|
||||
$fallback = is_string($fallback) ? trim($fallback) : null;
|
||||
|
||||
return $fallback !== '' ? $fallback : null;
|
||||
}),
|
||||
TextEntry::make('subject_type')->label('Subject type'),
|
||||
TextEntry::make('subject_external_id')->label('External ID')->copyable(),
|
||||
TextEntry::make('baseline_operation_run_id')
|
||||
@ -372,7 +385,19 @@ public static function table(Table $table): Table
|
||||
default => 'gray',
|
||||
})
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('subject_display_name')->label('Subject')->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('subject_display_name')
|
||||
->label('Subject')
|
||||
->placeholder('—')
|
||||
->formatStateUsing(function (?string $state, Finding $record): ?string {
|
||||
if (is_string($state) && trim($state) !== '') {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$fallback = Arr::get($record->evidence_jsonb ?? [], 'display_name');
|
||||
$fallback = is_string($fallback) ? trim($fallback) : null;
|
||||
|
||||
return $fallback !== '' ? $fallback : null;
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('subject_type')->label('Subject type')->searchable(),
|
||||
Tables\Columns\TextColumn::make('due_at')
|
||||
->label('Due')
|
||||
|
||||
@ -259,6 +259,130 @@ public static function infolist(Schema $schema): Schema
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Baseline compare evidence')
|
||||
->schema([
|
||||
TextEntry::make('baseline_compare_subjects_total')
|
||||
->label('Subjects total')
|
||||
->getStateUsing(function (OperationRun $record): ?int {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.subjects_total');
|
||||
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
})
|
||||
->placeholder('—'),
|
||||
TextEntry::make('baseline_compare_gap_count')
|
||||
->label('Evidence gaps')
|
||||
->getStateUsing(function (OperationRun $record): ?int {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.evidence_gaps.count');
|
||||
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
})
|
||||
->placeholder('—'),
|
||||
TextEntry::make('baseline_compare_resume_token')
|
||||
->label('Resume token')
|
||||
->getStateUsing(function (OperationRun $record): ?string {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.resume_token');
|
||||
|
||||
return is_string($value) && $value !== '' ? $value : null;
|
||||
})
|
||||
->copyable()
|
||||
->placeholder('—')
|
||||
->columnSpanFull()
|
||||
->visible(function (OperationRun $record): bool {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.resume_token');
|
||||
|
||||
return is_string($value) && $value !== '';
|
||||
}),
|
||||
ViewEntry::make('baseline_compare_evidence_capture')
|
||||
->label('Evidence capture')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(function (OperationRun $record): array {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.evidence_capture');
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
})
|
||||
->columnSpanFull(),
|
||||
ViewEntry::make('baseline_compare_evidence_gaps')
|
||||
->label('Evidence gaps')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(function (OperationRun $record): array {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_compare.evidence_gaps');
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
})
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->visible(fn (OperationRun $record): bool => (string) $record->type === 'baseline_compare')
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Baseline capture evidence')
|
||||
->schema([
|
||||
TextEntry::make('baseline_capture_subjects_total')
|
||||
->label('Subjects total')
|
||||
->getStateUsing(function (OperationRun $record): ?int {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.subjects_total');
|
||||
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
})
|
||||
->placeholder('—'),
|
||||
TextEntry::make('baseline_capture_gap_count')
|
||||
->label('Gaps')
|
||||
->getStateUsing(function (OperationRun $record): ?int {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.gaps.count');
|
||||
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
})
|
||||
->placeholder('—'),
|
||||
TextEntry::make('baseline_capture_resume_token')
|
||||
->label('Resume token')
|
||||
->getStateUsing(function (OperationRun $record): ?string {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.resume_token');
|
||||
|
||||
return is_string($value) && $value !== '' ? $value : null;
|
||||
})
|
||||
->copyable()
|
||||
->placeholder('—')
|
||||
->columnSpanFull()
|
||||
->visible(function (OperationRun $record): bool {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.resume_token');
|
||||
|
||||
return is_string($value) && $value !== '';
|
||||
}),
|
||||
ViewEntry::make('baseline_capture_evidence_capture')
|
||||
->label('Evidence capture')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(function (OperationRun $record): array {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.evidence_capture');
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
})
|
||||
->columnSpanFull(),
|
||||
ViewEntry::make('baseline_capture_gaps')
|
||||
->label('Gaps')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(function (OperationRun $record): array {
|
||||
$context = is_array($record->context) ? $record->context : [];
|
||||
$value = data_get($context, 'baseline_capture.gaps');
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
})
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->visible(fn (OperationRun $record): bool => (string) $record->type === 'baseline_capture')
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Verification report')
|
||||
->schema([
|
||||
ViewEntry::make('verification_report')
|
||||
|
||||
@ -15,6 +15,7 @@ final class BaselineCompareStats
|
||||
/**
|
||||
* @param array<string, int> $severityCounts
|
||||
* @param list<string> $uncoveredTypes
|
||||
* @param array<string, int> $evidenceGapsTopReasons
|
||||
*/
|
||||
private function __construct(
|
||||
public readonly string $state,
|
||||
@ -32,6 +33,8 @@ private function __construct(
|
||||
public readonly ?int $uncoveredTypesCount = null,
|
||||
public readonly array $uncoveredTypes = [],
|
||||
public readonly ?string $fidelity = null,
|
||||
public readonly ?int $evidenceGapsCount = null,
|
||||
public readonly array $evidenceGapsTopReasons = [],
|
||||
) {}
|
||||
|
||||
public static function forTenant(?Tenant $tenant): self
|
||||
@ -80,6 +83,7 @@ public static function forTenant(?Tenant $tenant): self
|
||||
->first();
|
||||
|
||||
[$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun);
|
||||
[$evidenceGapsCount, $evidenceGapsTopReasons] = self::evidenceGapSummaryForRun($latestRun);
|
||||
|
||||
// Active run (queued/running)
|
||||
if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) {
|
||||
@ -99,6 +103,8 @@ public static function forTenant(?Tenant $tenant): self
|
||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||
uncoveredTypes: $uncoveredTypes,
|
||||
fidelity: $fidelity,
|
||||
evidenceGapsCount: $evidenceGapsCount,
|
||||
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,6 +131,8 @@ public static function forTenant(?Tenant $tenant): self
|
||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||
uncoveredTypes: $uncoveredTypes,
|
||||
fidelity: $fidelity,
|
||||
evidenceGapsCount: $evidenceGapsCount,
|
||||
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||
);
|
||||
}
|
||||
|
||||
@ -173,6 +181,8 @@ public static function forTenant(?Tenant $tenant): self
|
||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||
uncoveredTypes: $uncoveredTypes,
|
||||
fidelity: $fidelity,
|
||||
evidenceGapsCount: $evidenceGapsCount,
|
||||
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||
);
|
||||
}
|
||||
|
||||
@ -195,6 +205,8 @@ public static function forTenant(?Tenant $tenant): self
|
||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||
uncoveredTypes: $uncoveredTypes,
|
||||
fidelity: $fidelity,
|
||||
evidenceGapsCount: $evidenceGapsCount,
|
||||
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||
);
|
||||
}
|
||||
|
||||
@ -214,6 +226,8 @@ public static function forTenant(?Tenant $tenant): self
|
||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||
uncoveredTypes: $uncoveredTypes,
|
||||
fidelity: $fidelity,
|
||||
evidenceGapsCount: $evidenceGapsCount,
|
||||
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||
);
|
||||
}
|
||||
|
||||
@ -321,6 +335,59 @@ private static function coverageInfoForRun(?OperationRun $run): array
|
||||
return [$coverageStatus, $uncoveredTypes, $fidelity];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ?int, 1: array<string, int>}
|
||||
*/
|
||||
private static function evidenceGapSummaryForRun(?OperationRun $run): array
|
||||
{
|
||||
if (! $run instanceof OperationRun) {
|
||||
return [null, []];
|
||||
}
|
||||
|
||||
$context = is_array($run->context) ? $run->context : [];
|
||||
$baselineCompare = $context['baseline_compare'] ?? null;
|
||||
|
||||
if (! is_array($baselineCompare)) {
|
||||
return [null, []];
|
||||
}
|
||||
|
||||
$gaps = $baselineCompare['evidence_gaps'] ?? null;
|
||||
|
||||
if (! is_array($gaps)) {
|
||||
return [null, []];
|
||||
}
|
||||
|
||||
$count = $gaps['count'] ?? null;
|
||||
$count = is_numeric($count) ? (int) $count : null;
|
||||
|
||||
$byReason = $gaps['by_reason'] ?? null;
|
||||
$byReason = is_array($byReason) ? $byReason : [];
|
||||
|
||||
$normalized = [];
|
||||
|
||||
foreach ($byReason as $reason => $value) {
|
||||
if (! is_string($reason) || trim($reason) === '' || ! is_numeric($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$intValue = (int) $value;
|
||||
|
||||
if ($intValue <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[trim($reason)] = $intValue;
|
||||
}
|
||||
|
||||
if ($count === null) {
|
||||
$count = array_sum($normalized);
|
||||
}
|
||||
|
||||
arsort($normalized);
|
||||
|
||||
return [$count, array_slice($normalized, 0, 6, true)];
|
||||
}
|
||||
|
||||
private static function empty(
|
||||
string $state,
|
||||
?string $message,
|
||||
|
||||
@ -6,6 +6,29 @@
|
||||
|
||||
@php
|
||||
$hasCoverageWarnings = in_array(($coverageStatus ?? null), ['warning', 'unproven'], true);
|
||||
$evidenceGapsCountValue = (int) ($evidenceGapsCount ?? 0);
|
||||
$hasEvidenceGaps = $evidenceGapsCountValue > 0;
|
||||
$hasWarnings = $hasCoverageWarnings || $hasEvidenceGaps;
|
||||
|
||||
$evidenceGapsSummary = null;
|
||||
$evidenceGapsTooltip = null;
|
||||
|
||||
if ($hasEvidenceGaps && is_array($evidenceGapsTopReasons ?? null) && $evidenceGapsTopReasons !== []) {
|
||||
$parts = [];
|
||||
|
||||
foreach (array_slice($evidenceGapsTopReasons, 0, 5, true) as $reason => $count) {
|
||||
if (! is_string($reason) || $reason === '' || ! is_numeric($count)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = $reason.' ('.((int) $count).')';
|
||||
}
|
||||
|
||||
if ($parts !== []) {
|
||||
$evidenceGapsSummary = implode(', ', $parts);
|
||||
$evidenceGapsTooltip = 'Top gaps: '.$evidenceGapsSummary;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- Row 1: Stats Overview --}}
|
||||
@ -38,7 +61,19 @@ class="w-fit"
|
||||
Fidelity: {{ Str::title($fidelity) }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($hasEvidenceGaps)
|
||||
<x-filament::badge color="warning" size="sm" class="w-fit" :title="$evidenceGapsTooltip">
|
||||
Evidence gaps: {{ $evidenceGapsCountValue }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($hasEvidenceGaps && filled($evidenceGapsSummary))
|
||||
<div class="mt-1 text-xs text-warning-700 dark:text-warning-300">
|
||||
Top gaps: {{ $evidenceGapsSummary }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@ -49,7 +84,7 @@ class="w-fit"
|
||||
@if ($state === 'failed')
|
||||
<div class="text-lg font-semibold text-danger-600 dark:text-danger-400">Error</div>
|
||||
@else
|
||||
<div class="text-3xl font-bold {{ ($findingsCount ?? 0) > 0 ? 'text-danger-600 dark:text-danger-400' : ($hasCoverageWarnings ? 'text-warning-600 dark:text-warning-400' : 'text-success-600 dark:text-success-400') }}">
|
||||
<div class="text-3xl font-bold {{ ($findingsCount ?? 0) > 0 ? 'text-danger-600 dark:text-danger-400' : ($hasWarnings ? 'text-warning-600 dark:text-warning-400' : 'text-success-600 dark:text-success-400') }}">
|
||||
{{ $findingsCount ?? 0 }}
|
||||
</div>
|
||||
@endif
|
||||
@ -58,10 +93,12 @@ class="w-fit"
|
||||
<x-filament::loading-indicator class="h-3 w-3" />
|
||||
Comparing…
|
||||
</div>
|
||||
@elseif (($findingsCount ?? 0) === 0 && $state === 'ready' && ! $hasCoverageWarnings)
|
||||
@elseif (($findingsCount ?? 0) === 0 && $state === 'ready' && ! $hasWarnings)
|
||||
<span class="text-sm text-success-600 dark:text-success-400">All clear</span>
|
||||
@elseif ($state === 'ready' && $hasCoverageWarnings)
|
||||
<span class="text-sm text-warning-600 dark:text-warning-400">Coverage warnings</span>
|
||||
@elseif ($state === 'ready' && $hasEvidenceGaps)
|
||||
<span class="text-sm text-warning-600 dark:text-warning-400">Evidence gaps</span>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@ -107,27 +107,27 @@ ## Phase 4: User Story 2 — Compare now with full content and get explainable d
|
||||
|
||||
### Tests (write first)
|
||||
|
||||
- [ ] T040 [P] [US2] Add cross-tenant match test (policy_type + `subject_key`) in `tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php`
|
||||
- [ ] T041 [P] [US2] Add ambiguous match suppression test in `tests/Feature/Baselines/BaselineCompareAmbiguousMatchGapTest.php` (duplicate `subject_key` values → evidence gap; no finding)
|
||||
- [ ] T042 [P] [US2] Add coverage proof guard test in `tests/Feature/Baselines/BaselineCompareCoverageProofGuardTest.php` (uncovered types suppress `missing_policy` outcomes; run completes with warnings + records context)
|
||||
- [ ] T043 [P] [US2] Add stable recurrence identity test in `tests/Feature/Baselines/BaselineCompareFindingRecurrenceKeyTest.php` (recurrence key independent of hashes; retries don’t duplicate; lifecycle fields update)
|
||||
- [ ] T044 [P] [US2] Update compare start surface expectations for full-content labeling + rollout gating in `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`
|
||||
- [ ] T045 [P] [US2] Add baseline profile “Compare now (full content)” start-surface test in `tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`
|
||||
- [ ] T046 [P] [US2] Add audit event coverage for baseline compare start/completion in `tests/Feature/Baselines/BaselineCompareAuditEventsTest.php` (purpose, scope counts, gaps/warnings summary)
|
||||
- [X] T040 [P] [US2] Add cross-tenant match test (policy_type + `subject_key`) in `tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php`
|
||||
- [X] T041 [P] [US2] Add ambiguous match suppression test in `tests/Feature/Baselines/BaselineCompareAmbiguousMatchGapTest.php` (duplicate `subject_key` values → evidence gap; no finding)
|
||||
- [X] T042 [P] [US2] Add coverage proof guard test in `tests/Feature/Baselines/BaselineCompareCoverageProofGuardTest.php` (uncovered types suppress `missing_policy` outcomes; run completes with warnings + records context)
|
||||
- [X] T043 [P] [US2] Add stable recurrence identity test in `tests/Feature/Baselines/BaselineCompareFindingRecurrenceKeyTest.php` (recurrence key independent of hashes; retries don’t duplicate; lifecycle fields update)
|
||||
- [X] T044 [P] [US2] Update compare start surface expectations for full-content labeling + rollout gating in `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`
|
||||
- [X] T045 [P] [US2] Add baseline profile “Compare now (full content)” start-surface test in `tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`
|
||||
- [X] T046 [P] [US2] Add audit event coverage for baseline compare start/completion in `tests/Feature/Baselines/BaselineCompareAuditEventsTest.php` (purpose, scope counts, gaps/warnings summary)
|
||||
|
||||
### Implementation
|
||||
|
||||
- [ ] T047 [US2] Add “Compare now (full content)” header action to baseline profile view in `app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` (select target tenant; require `tenant.sync`; enforce rollout gate server-side)
|
||||
- [ ] T048 [US2] Integrate `BaselineContentCapturePhase` refresh into compare in `app/Jobs/CompareBaselineToTenantJob.php` (purpose `baseline_compare`, budgeted, record `context.baseline_compare.evidence_capture`, `context.baseline_compare.evidence_gaps`, `context.baseline_compare.resume_token`, and add job-level rollout gate guard)
|
||||
- [ ] T049 [US2] Switch compare matching to `policy_type + subject_key` in `app/Jobs/CompareBaselineToTenantJob.php` (load baseline items by `subject_key`; compute current `subject_key` from inventory display name; detect missing/empty/duplicate keys on either side; record gap reasons; suppress drift evaluation for those keys)
|
||||
- [ ] T050 [US2] Enforce coverage proof guard behavior in `app/Jobs/CompareBaselineToTenantJob.php` (suppress `missing_policy` for uncovered/unproven types; record warning + `BaselineCompareReasonCode` when suppression affects outcomes)
|
||||
- [ ] T051 [US2] Update finding recurrence identity to be stable and independent of hashes in `app/Jobs/CompareBaselineToTenantJob.php` (recurrence key uses tenant_id + baseline_profile_id + policy_type + subject_key + change_type; retries must not duplicate findings)
|
||||
- [ ] T052 [US2] Ensure findings carry `subject_key` + `display_name` fallbacks in `evidence_jsonb` and update subject display name fallback logic in `app/Filament/Resources/FindingResource.php` (COALESCE inventory display name with evidence display name)
|
||||
- [ ] T053 [US2] Ensure compare run context contains scope totals, processed counts, coverage proof status, fidelity breakdown, evidence capture stats, and top gap reasons in `app/Jobs/CompareBaselineToTenantJob.php`
|
||||
- [ ] T054 [US2] Update baseline compare landing to label “Compare now (full content)” when applicable in `app/Filament/Pages/BaselineCompareLanding.php` and `resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||
- [ ] T055 [US2] Extend stats DTO to surface fidelity + evidence gap summary from run context in `app/Support/Baselines/BaselineCompareStats.php`
|
||||
- [ ] T056 [US2] Add evidence capture + gaps panels for baseline capture/compare runs in Monitoring detail in `app/Filament/Resources/OperationRunResource.php`
|
||||
- [ ] T057 [US2] Expand compare audit events to include purpose, scope counts, evidence capture stats, and gaps/warnings summary in `app/Jobs/CompareBaselineToTenantJob.php`
|
||||
- [X] T047 [US2] Add “Compare now (full content)” header action to baseline profile view in `app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` (select target tenant; require `tenant.sync`; enforce rollout gate server-side)
|
||||
- [X] T048 [US2] Integrate `BaselineContentCapturePhase` refresh into compare in `app/Jobs/CompareBaselineToTenantJob.php` (purpose `baseline_compare`, budgeted, record `context.baseline_compare.evidence_capture`, `context.baseline_compare.evidence_gaps`, `context.baseline_compare.resume_token`, and add job-level rollout gate guard)
|
||||
- [X] T049 [US2] Switch compare matching to `policy_type + subject_key` in `app/Jobs/CompareBaselineToTenantJob.php` (load baseline items by `subject_key`; compute current `subject_key` from inventory display name; detect missing/empty/duplicate keys on either side; record gap reasons; suppress drift evaluation for those keys)
|
||||
- [X] T050 [US2] Enforce coverage proof guard behavior in `app/Jobs/CompareBaselineToTenantJob.php` (suppress `missing_policy` for uncovered/unproven types; record warning + `BaselineCompareReasonCode` when suppression affects outcomes)
|
||||
- [X] T051 [US2] Update finding recurrence identity to be stable and independent of hashes in `app/Jobs/CompareBaselineToTenantJob.php` (recurrence key uses tenant_id + baseline_profile_id + policy_type + subject_key + change_type; retries must not duplicate findings)
|
||||
- [X] T052 [US2] Ensure findings carry `subject_key` + `display_name` fallbacks in `evidence_jsonb` and update subject display name fallback logic in `app/Filament/Resources/FindingResource.php` (COALESCE inventory display name with evidence display name)
|
||||
- [X] T053 [US2] Ensure compare run context contains scope totals, processed counts, coverage proof status, fidelity breakdown, evidence capture stats, and top gap reasons in `app/Jobs/CompareBaselineToTenantJob.php`
|
||||
- [X] T054 [US2] Update baseline compare landing to label “Compare now (full content)” when applicable in `app/Filament/Pages/BaselineCompareLanding.php` and `resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||
- [X] T055 [US2] Extend stats DTO to surface fidelity + evidence gap summary from run context in `app/Support/Baselines/BaselineCompareStats.php`
|
||||
- [X] T056 [US2] Add evidence capture + gaps panels for baseline capture/compare runs in Monitoring detail in `app/Filament/Resources/OperationRunResource.php`
|
||||
- [X] T057 [US2] Expand compare audit events to include purpose, scope counts, evidence capture stats, and gaps/warnings summary in `app/Jobs/CompareBaselineToTenantJob.php`
|
||||
|
||||
**Parallel execution example (US2)**:
|
||||
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
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\SettingsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
use App\Support\OperationRunType;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
@ -53,18 +56,27 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshot, 'deviceConfiguration', 'windows'),
|
||||
);
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => [
|
||||
'display_name' => 'Policy A',
|
||||
'display_name' => (string) $policy->display_name,
|
||||
'evidence' => [
|
||||
'fidelity' => 'content',
|
||||
'source' => 'policy_version',
|
||||
@ -168,18 +180,27 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
);
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => [
|
||||
'display_name' => 'Policy B',
|
||||
'display_name' => (string) $policy->display_name,
|
||||
'evidence' => [
|
||||
'fidelity' => 'content',
|
||||
'source' => 'policy_version',
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
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\SettingsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
use App\Support\OperationRunType;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
@ -68,14 +71,21 @@
|
||||
metaJsonb: is_array($inventory->meta_jsonb) ? $inventory->meta_jsonb : [],
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineMetaHash,
|
||||
'meta_jsonb' => [
|
||||
'display_name' => 'Policy Meta',
|
||||
'display_name' => (string) $policy->display_name,
|
||||
'evidence' => [
|
||||
'fidelity' => 'meta',
|
||||
'source' => 'inventory',
|
||||
@ -123,7 +133,7 @@
|
||||
$context = is_array($run->context) ? $run->context : [];
|
||||
|
||||
expect(data_get($context, 'baseline_compare.fidelity'))->toBe('meta');
|
||||
expect(data_get($context, 'baseline_compare.coverage.resolved_content'))->toBe(1);
|
||||
expect(data_get($context, 'baseline_compare.coverage.resolved_meta'))->toBe(1);
|
||||
expect(data_get($context, 'baseline_compare.coverage.baseline_meta'))->toBe(1);
|
||||
});
|
||||
|
||||
@ -164,18 +174,27 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineContentHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
);
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineContentHash,
|
||||
'meta_jsonb' => [
|
||||
'display_name' => 'Policy Content',
|
||||
'display_name' => (string) $policy->display_name,
|
||||
'evidence' => [
|
||||
'fidelity' => 'content',
|
||||
'source' => 'policy_version',
|
||||
@ -234,7 +253,7 @@
|
||||
$context = is_array($run->context) ? $run->context : [];
|
||||
|
||||
expect(data_get($context, 'baseline_compare.fidelity'))->toBe('meta');
|
||||
expect(data_get($context, 'baseline_compare.coverage.resolved_meta'))->toBe(1);
|
||||
expect(data_get($context, 'baseline_compare.coverage.resolved_meta'))->toBe(0);
|
||||
expect(data_get($context, 'baseline_compare.coverage.baseline_content'))->toBe(1);
|
||||
expect(data_get($context, 'baseline_compare.evidence_gaps.missing_current'))->toBe(1);
|
||||
});
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
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\SettingsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
use App\Support\OperationRunType;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
@ -51,14 +54,23 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
);
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => [
|
||||
@ -169,10 +181,17 @@
|
||||
metaJsonb: $baselineMetaJsonb,
|
||||
);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => [
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
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\SettingsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
use App\Support\OperationRunType;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
@ -51,18 +54,27 @@
|
||||
],
|
||||
];
|
||||
|
||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
||||
);
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||
expect($subjectKey)->not->toBeNull();
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => (string) $policy->external_id,
|
||||
'subject_external_id' => BaselineSubjectKey::workspaceSafeSubjectExternalId(
|
||||
policyType: (string) $policy->policy_type,
|
||||
subjectKey: (string) $subjectKey,
|
||||
),
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => (string) $policy->policy_type,
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => [
|
||||
'display_name' => 'Policy Provenance',
|
||||
'display_name' => (string) $policy->display_name,
|
||||
'evidence' => [
|
||||
'fidelity' => 'content',
|
||||
'source' => 'policy_version',
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
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\SettingsNormalizer;
|
||||
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
it('Baseline resolver prefers content evidence over meta evidence when available', function () {
|
||||
@ -46,13 +48,15 @@
|
||||
'last_seen_operation_run_id' => null,
|
||||
]);
|
||||
|
||||
$expectedContentHash = app(DriftHasher::class)->hashNormalized(
|
||||
app(SettingsNormalizer::class)->normalizeForDiff(
|
||||
$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([]),
|
||||
]);
|
||||
|
||||
$expectedMetaHash = app(BaselineSnapshotIdentity::class)->hashItemContent(
|
||||
policyType: (string) $inventory->policy_type,
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Services\Drift\DriftHasher;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\OperationRunService;
|
||||
use App\Support\Baselines\BaselineSubjectKey;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
@ -43,13 +44,19 @@
|
||||
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE'],
|
||||
);
|
||||
|
||||
$coveredDisplayName = 'Covered Policy';
|
||||
$coveredSubjectKey = BaselineSubjectKey::fromDisplayName($coveredDisplayName);
|
||||
expect($coveredSubjectKey)->not->toBeNull();
|
||||
$coveredWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $coveredSubjectKey);
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => 'covered-uuid',
|
||||
'subject_external_id' => $coveredWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $coveredSubjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $hasher->hashNormalized($coveredContract),
|
||||
'meta_jsonb' => ['display_name' => 'Covered Policy'],
|
||||
'meta_jsonb' => ['display_name' => $coveredDisplayName],
|
||||
]);
|
||||
|
||||
$uncoveredContract = $builder->build(
|
||||
@ -58,13 +65,19 @@
|
||||
metaJsonb: ['odata_type' => '#microsoft.graph.deviceCompliancePolicy', 'etag' => 'E_BASELINE'],
|
||||
);
|
||||
|
||||
$uncoveredDisplayName = 'Uncovered Policy';
|
||||
$uncoveredSubjectKey = BaselineSubjectKey::fromDisplayName($uncoveredDisplayName);
|
||||
expect($uncoveredSubjectKey)->not->toBeNull();
|
||||
$uncoveredWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceCompliancePolicy', (string) $uncoveredSubjectKey);
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => 'uncovered-uuid',
|
||||
'subject_external_id' => $uncoveredWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $uncoveredSubjectKey,
|
||||
'policy_type' => 'deviceCompliancePolicy',
|
||||
'baseline_hash' => $hasher->hashNormalized($uncoveredContract),
|
||||
'meta_jsonb' => ['display_name' => 'Uncovered Policy'],
|
||||
'meta_jsonb' => ['display_name' => $uncoveredDisplayName],
|
||||
]);
|
||||
|
||||
$inventorySyncRun = OperationRun::factory()->create([
|
||||
@ -93,7 +106,7 @@
|
||||
'external_id' => 'covered-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT'],
|
||||
'display_name' => 'Covered Policy Changed',
|
||||
'display_name' => $coveredDisplayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
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;
|
||||
|
||||
@ -42,22 +43,46 @@
|
||||
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' => 'policy-a-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'content-a'),
|
||||
'meta_jsonb' => ['display_name' => 'Policy A'],
|
||||
'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' => 'policy-b-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'content-b'),
|
||||
'meta_jsonb' => ['display_name' => 'Policy B'],
|
||||
'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)
|
||||
@ -65,9 +90,9 @@
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'workspace_id' => $tenant->workspace_id,
|
||||
'external_id' => 'policy-a-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['different_content' => true],
|
||||
'display_name' => 'Policy A modified',
|
||||
'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(),
|
||||
]);
|
||||
@ -75,9 +100,9 @@
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'workspace_id' => $tenant->workspace_id,
|
||||
'external_id' => 'policy-c-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['new_policy' => true],
|
||||
'display_name' => 'Policy C unexpected',
|
||||
'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(),
|
||||
]);
|
||||
@ -157,30 +182,54 @@
|
||||
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' => 'policy-a-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'content-a'),
|
||||
'meta_jsonb' => ['display_name' => 'Policy A'],
|
||||
'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' => 'policy-b-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'content-b'),
|
||||
'meta_jsonb' => ['display_name' => 'Policy B'],
|
||||
'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' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['different_content' => true],
|
||||
'display_name' => 'Policy A modified',
|
||||
'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(),
|
||||
]);
|
||||
@ -266,13 +315,19 @@
|
||||
],
|
||||
);
|
||||
|
||||
$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' => 'settings-catalog-policy-uuid',
|
||||
'subject_external_id' => $workspaceSafeExternalId,
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => 'settingsCatalogPolicy',
|
||||
'baseline_hash' => hash('sha256', 'content-a'),
|
||||
'meta_jsonb' => ['display_name' => 'Settings Catalog A'],
|
||||
'meta_jsonb' => ['display_name' => $displayName],
|
||||
]);
|
||||
|
||||
// Inventory item exists, but it was NOT observed in the latest sync run.
|
||||
@ -281,7 +336,7 @@
|
||||
'workspace_id' => $tenant->workspace_id,
|
||||
'external_id' => 'settings-catalog-policy-uuid',
|
||||
'policy_type' => 'settingsCatalogPolicy',
|
||||
'display_name' => 'Settings Catalog A',
|
||||
'display_name' => $displayName,
|
||||
'meta_jsonb' => ['etag' => 'abc'],
|
||||
'last_seen_operation_run_id' => (int) $olderInventoryRun->getKey(),
|
||||
'last_seen_at' => now()->subMinutes(5),
|
||||
@ -351,13 +406,19 @@
|
||||
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' => 'policy-x-uuid',
|
||||
'subject_external_id' => $workspaceSafeExternalId,
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $hasher->hashNormalized($baselineContract),
|
||||
'meta_jsonb' => ['display_name' => 'Policy X'],
|
||||
'meta_jsonb' => ['display_name' => $displayName],
|
||||
]);
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
@ -366,7 +427,7 @@
|
||||
'external_id' => 'policy-x-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_1'],
|
||||
'display_name' => 'Policy X modified',
|
||||
'display_name' => $displayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
@ -453,7 +514,7 @@
|
||||
expect((string) ($finding->evidence_jsonb['current_hash'] ?? ''))->not->toBe($currentHash1);
|
||||
});
|
||||
|
||||
it('creates new finding identities when a new snapshot is captured (snapshot-scoped recurrence)', function () {
|
||||
it('does not create new finding identities when a new snapshot is captured', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$profile = BaselineProfile::factory()->active()->create([
|
||||
@ -476,6 +537,11 @@
|
||||
);
|
||||
$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(),
|
||||
@ -484,9 +550,11 @@
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => $snapshot1->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => 'policy-x-uuid',
|
||||
'subject_external_id' => $workspaceSafeExternalId,
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => ['display_name' => $displayName],
|
||||
]);
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
@ -495,6 +563,7 @@
|
||||
'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(),
|
||||
]);
|
||||
@ -520,13 +589,14 @@
|
||||
|
||||
$scopeKey = 'baseline_profile:'.$profile->getKey();
|
||||
|
||||
$fingerprint1 = (string) Finding::query()
|
||||
$finding = Finding::query()
|
||||
->where('tenant_id', $tenant->getKey())
|
||||
->where('source', 'baseline.compare')
|
||||
->where('scope_key', $scopeKey)
|
||||
->orderBy('id')
|
||||
->firstOrFail()
|
||||
->fingerprint;
|
||||
->sole();
|
||||
|
||||
expect($finding->times_seen)->toBe(1);
|
||||
$fingerprint1 = (string) $finding->fingerprint;
|
||||
|
||||
$snapshot2 = BaselineSnapshot::factory()->create([
|
||||
'workspace_id' => $tenant->workspace_id,
|
||||
@ -536,9 +606,11 @@
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => $snapshot2->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => 'policy-x-uuid',
|
||||
'subject_external_id' => $workspaceSafeExternalId,
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $baselineHash,
|
||||
'meta_jsonb' => ['display_name' => $displayName],
|
||||
]);
|
||||
|
||||
$run2 = $opService->ensureRunWithIdentity(
|
||||
@ -566,9 +638,9 @@
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
expect($findings)->toHaveCount(2);
|
||||
expect($findings->pluck('fingerprint')->unique()->count())->toBe(2);
|
||||
expect($findings->pluck('fingerprint')->all())->toContain($fingerprint1);
|
||||
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 () {
|
||||
@ -607,13 +679,19 @@
|
||||
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' => 'matching-uuid',
|
||||
'subject_external_id' => $workspaceSafeExternalId,
|
||||
'subject_key' => (string) $subjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $contentHash,
|
||||
'meta_jsonb' => ['display_name' => 'Matching Policy'],
|
||||
'meta_jsonb' => ['display_name' => $displayName],
|
||||
]);
|
||||
|
||||
// Tenant inventory with same content → same hash
|
||||
@ -623,7 +701,7 @@
|
||||
'external_id' => 'matching-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => $metaContent,
|
||||
'display_name' => 'Matching Policy',
|
||||
'display_name' => $displayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
@ -698,22 +776,37 @@
|
||||
metaJsonb: $metaContent,
|
||||
));
|
||||
|
||||
BaselineSnapshotItem::factory()->create([
|
||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||
'subject_type' => 'policy',
|
||||
'subject_external_id' => 'matching-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => $contentHash,
|
||||
'meta_jsonb' => ['display_name' => 'Matching Policy'],
|
||||
]);
|
||||
$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' => 'foundation-uuid',
|
||||
'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' => 'Foundation Template'],
|
||||
'meta_jsonb' => ['display_name' => $foundationDisplayName],
|
||||
]);
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
@ -722,7 +815,7 @@
|
||||
'external_id' => 'matching-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => $metaContent,
|
||||
'display_name' => 'Matching Policy',
|
||||
'display_name' => $displayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
@ -733,7 +826,7 @@
|
||||
'external_id' => 'foundation-uuid',
|
||||
'policy_type' => 'notificationMessageTemplate',
|
||||
'meta_jsonb' => ['some' => 'value'],
|
||||
'display_name' => 'Foundation Template',
|
||||
'display_name' => $foundationDisplayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
@ -795,21 +888,34 @@
|
||||
);
|
||||
|
||||
// 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' => 'missing-uuid',
|
||||
'subject_external_id' => $missingWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $missingSubjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'missing-content'),
|
||||
'meta_jsonb' => ['display_name' => 'Missing Policy'],
|
||||
'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' => 'changed-uuid',
|
||||
'subject_external_id' => $changedWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $changedSubjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'original-content'),
|
||||
'meta_jsonb' => ['display_name' => 'Changed Policy'],
|
||||
'meta_jsonb' => ['display_name' => $changedDisplayName],
|
||||
]);
|
||||
|
||||
// Tenant only has changed-uuid with different content + extra-uuid (unexpected)
|
||||
@ -819,7 +925,7 @@
|
||||
'external_id' => 'changed-uuid',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['modified_content' => true],
|
||||
'display_name' => 'Changed Policy',
|
||||
'display_name' => $changedDisplayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
@ -1126,21 +1232,34 @@
|
||||
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' => 'missing-policy',
|
||||
'subject_external_id' => $missingWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $missingSubjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'baseline-a'),
|
||||
'meta_jsonb' => ['display_name' => 'Missing Policy'],
|
||||
'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' => 'different-policy',
|
||||
'subject_external_id' => $differentWorkspaceSafeExternalId,
|
||||
'subject_key' => (string) $differentSubjectKey,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'baseline_hash' => hash('sha256', 'baseline-b'),
|
||||
'meta_jsonb' => ['display_name' => 'Different Policy'],
|
||||
'meta_jsonb' => ['display_name' => $differentDisplayName],
|
||||
]);
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
@ -1149,7 +1268,7 @@
|
||||
'external_id' => 'different-policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'meta_jsonb' => ['different_content' => true],
|
||||
'display_name' => 'Different Policy',
|
||||
'display_name' => $differentDisplayName,
|
||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user