fix: align compare UI + drift engine tests with subject_key
This commit is contained in:
parent
3e0dc438f7
commit
559bba09a0
@ -72,6 +72,11 @@ class BaselineCompareLanding extends Page
|
|||||||
|
|
||||||
public ?string $fidelity = null;
|
public ?string $fidelity = null;
|
||||||
|
|
||||||
|
public ?int $evidenceGapsCount = null;
|
||||||
|
|
||||||
|
/** @var array<string, int>|null */
|
||||||
|
public ?array $evidenceGapsTopReasons = null;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@ -116,6 +121,9 @@ public function refreshStats(): void
|
|||||||
$this->uncoveredTypesCount = $stats->uncoveredTypesCount;
|
$this->uncoveredTypesCount = $stats->uncoveredTypesCount;
|
||||||
$this->uncoveredTypes = $stats->uncoveredTypes !== [] ? $stats->uncoveredTypes : null;
|
$this->uncoveredTypes = $stats->uncoveredTypes !== [] ? $stats->uncoveredTypes : null;
|
||||||
$this->fidelity = $stats->fidelity;
|
$this->fidelity = $stats->fidelity;
|
||||||
|
|
||||||
|
$this->evidenceGapsCount = $stats->evidenceGapsCount;
|
||||||
|
$this->evidenceGapsTopReasons = $stats->evidenceGapsTopReasons !== [] ? $stats->evidenceGapsTopReasons : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||||
|
|||||||
@ -144,7 +144,20 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
||||||
TextEntry::make('fingerprint')->label('Fingerprint')->copyable(),
|
TextEntry::make('fingerprint')->label('Fingerprint')->copyable(),
|
||||||
TextEntry::make('scope_key')->label('Scope')->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_type')->label('Subject type'),
|
||||||
TextEntry::make('subject_external_id')->label('External ID')->copyable(),
|
TextEntry::make('subject_external_id')->label('External ID')->copyable(),
|
||||||
TextEntry::make('baseline_operation_run_id')
|
TextEntry::make('baseline_operation_run_id')
|
||||||
@ -372,7 +385,19 @@ public static function table(Table $table): Table
|
|||||||
default => 'gray',
|
default => 'gray',
|
||||||
})
|
})
|
||||||
->sortable(),
|
->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('subject_type')->label('Subject type')->searchable(),
|
||||||
Tables\Columns\TextColumn::make('due_at')
|
Tables\Columns\TextColumn::make('due_at')
|
||||||
->label('Due')
|
->label('Due')
|
||||||
|
|||||||
@ -259,6 +259,130 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->columns(2)
|
->columns(2)
|
||||||
->columnSpanFull(),
|
->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')
|
Section::make('Verification report')
|
||||||
->schema([
|
->schema([
|
||||||
ViewEntry::make('verification_report')
|
ViewEntry::make('verification_report')
|
||||||
|
|||||||
@ -15,6 +15,7 @@ final class BaselineCompareStats
|
|||||||
/**
|
/**
|
||||||
* @param array<string, int> $severityCounts
|
* @param array<string, int> $severityCounts
|
||||||
* @param list<string> $uncoveredTypes
|
* @param list<string> $uncoveredTypes
|
||||||
|
* @param array<string, int> $evidenceGapsTopReasons
|
||||||
*/
|
*/
|
||||||
private function __construct(
|
private function __construct(
|
||||||
public readonly string $state,
|
public readonly string $state,
|
||||||
@ -32,6 +33,8 @@ private function __construct(
|
|||||||
public readonly ?int $uncoveredTypesCount = null,
|
public readonly ?int $uncoveredTypesCount = null,
|
||||||
public readonly array $uncoveredTypes = [],
|
public readonly array $uncoveredTypes = [],
|
||||||
public readonly ?string $fidelity = null,
|
public readonly ?string $fidelity = null,
|
||||||
|
public readonly ?int $evidenceGapsCount = null,
|
||||||
|
public readonly array $evidenceGapsTopReasons = [],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function forTenant(?Tenant $tenant): self
|
public static function forTenant(?Tenant $tenant): self
|
||||||
@ -80,6 +83,7 @@ public static function forTenant(?Tenant $tenant): self
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
[$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun);
|
[$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun);
|
||||||
|
[$evidenceGapsCount, $evidenceGapsTopReasons] = self::evidenceGapSummaryForRun($latestRun);
|
||||||
|
|
||||||
// Active run (queued/running)
|
// Active run (queued/running)
|
||||||
if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) {
|
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,
|
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||||
uncoveredTypes: $uncoveredTypes,
|
uncoveredTypes: $uncoveredTypes,
|
||||||
fidelity: $fidelity,
|
fidelity: $fidelity,
|
||||||
|
evidenceGapsCount: $evidenceGapsCount,
|
||||||
|
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,6 +131,8 @@ public static function forTenant(?Tenant $tenant): self
|
|||||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||||
uncoveredTypes: $uncoveredTypes,
|
uncoveredTypes: $uncoveredTypes,
|
||||||
fidelity: $fidelity,
|
fidelity: $fidelity,
|
||||||
|
evidenceGapsCount: $evidenceGapsCount,
|
||||||
|
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +181,8 @@ public static function forTenant(?Tenant $tenant): self
|
|||||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||||
uncoveredTypes: $uncoveredTypes,
|
uncoveredTypes: $uncoveredTypes,
|
||||||
fidelity: $fidelity,
|
fidelity: $fidelity,
|
||||||
|
evidenceGapsCount: $evidenceGapsCount,
|
||||||
|
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +205,8 @@ public static function forTenant(?Tenant $tenant): self
|
|||||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||||
uncoveredTypes: $uncoveredTypes,
|
uncoveredTypes: $uncoveredTypes,
|
||||||
fidelity: $fidelity,
|
fidelity: $fidelity,
|
||||||
|
evidenceGapsCount: $evidenceGapsCount,
|
||||||
|
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +226,8 @@ public static function forTenant(?Tenant $tenant): self
|
|||||||
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
|
||||||
uncoveredTypes: $uncoveredTypes,
|
uncoveredTypes: $uncoveredTypes,
|
||||||
fidelity: $fidelity,
|
fidelity: $fidelity,
|
||||||
|
evidenceGapsCount: $evidenceGapsCount,
|
||||||
|
evidenceGapsTopReasons: $evidenceGapsTopReasons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +335,59 @@ private static function coverageInfoForRun(?OperationRun $run): array
|
|||||||
return [$coverageStatus, $uncoveredTypes, $fidelity];
|
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(
|
private static function empty(
|
||||||
string $state,
|
string $state,
|
||||||
?string $message,
|
?string $message,
|
||||||
|
|||||||
@ -6,6 +6,29 @@
|
|||||||
|
|
||||||
@php
|
@php
|
||||||
$hasCoverageWarnings = in_array(($coverageStatus ?? null), ['warning', 'unproven'], true);
|
$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
|
@endphp
|
||||||
|
|
||||||
{{-- Row 1: Stats Overview --}}
|
{{-- Row 1: Stats Overview --}}
|
||||||
@ -38,7 +61,19 @@ class="w-fit"
|
|||||||
Fidelity: {{ Str::title($fidelity) }}
|
Fidelity: {{ Str::title($fidelity) }}
|
||||||
</x-filament::badge>
|
</x-filament::badge>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if ($hasEvidenceGaps)
|
||||||
|
<x-filament::badge color="warning" size="sm" class="w-fit" :title="$evidenceGapsTooltip">
|
||||||
|
Evidence gaps: {{ $evidenceGapsCountValue }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($hasEvidenceGaps && filled($evidenceGapsSummary))
|
||||||
|
<div class="mt-1 text-xs text-warning-700 dark:text-warning-300">
|
||||||
|
Top gaps: {{ $evidenceGapsSummary }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
@ -49,7 +84,7 @@ class="w-fit"
|
|||||||
@if ($state === 'failed')
|
@if ($state === 'failed')
|
||||||
<div class="text-lg font-semibold text-danger-600 dark:text-danger-400">Error</div>
|
<div class="text-lg font-semibold text-danger-600 dark:text-danger-400">Error</div>
|
||||||
@else
|
@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 }}
|
{{ $findingsCount ?? 0 }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@ -58,10 +93,12 @@ class="w-fit"
|
|||||||
<x-filament::loading-indicator class="h-3 w-3" />
|
<x-filament::loading-indicator class="h-3 w-3" />
|
||||||
Comparing…
|
Comparing…
|
||||||
</div>
|
</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>
|
<span class="text-sm text-success-600 dark:text-success-400">All clear</span>
|
||||||
@elseif ($state === 'ready' && $hasCoverageWarnings)
|
@elseif ($state === 'ready' && $hasCoverageWarnings)
|
||||||
<span class="text-sm text-warning-600 dark:text-warning-400">Coverage warnings</span>
|
<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
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|||||||
@ -107,27 +107,27 @@ ## Phase 4: User Story 2 — Compare now with full content and get explainable d
|
|||||||
|
|
||||||
### Tests (write first)
|
### Tests (write first)
|
||||||
|
|
||||||
- [ ] T040 [P] [US2] Add cross-tenant match test (policy_type + `subject_key`) in `tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php`
|
- [X] 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)
|
- [X] 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)
|
- [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)
|
||||||
- [ ] 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] 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`
|
- [X] 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`
|
- [X] 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] 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
|
### 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)
|
- [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)
|
||||||
- [ ] 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] 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)
|
- [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)
|
||||||
- [ ] 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] 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)
|
- [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)
|
||||||
- [ ] 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] 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`
|
- [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`
|
||||||
- [ ] 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] 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`
|
- [X] 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`
|
- [X] 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] 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)**:
|
**Parallel execution example (US2)**:
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||||
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
@ -53,18 +56,27 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshot, 'deviceConfiguration', 'windows'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
'display_name' => 'Policy A',
|
'display_name' => (string) $policy->display_name,
|
||||||
'evidence' => [
|
'evidence' => [
|
||||||
'fidelity' => 'content',
|
'fidelity' => 'content',
|
||||||
'source' => 'policy_version',
|
'source' => 'policy_version',
|
||||||
@ -168,18 +180,27 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
'display_name' => 'Policy B',
|
'display_name' => (string) $policy->display_name,
|
||||||
'evidence' => [
|
'evidence' => [
|
||||||
'fidelity' => 'content',
|
'fidelity' => 'content',
|
||||||
'source' => 'policy_version',
|
'source' => 'policy_version',
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||||
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
@ -68,14 +71,21 @@
|
|||||||
metaJsonb: is_array($inventory->meta_jsonb) ? $inventory->meta_jsonb : [],
|
metaJsonb: is_array($inventory->meta_jsonb) ? $inventory->meta_jsonb : [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||||
|
expect($subjectKey)->not->toBeNull();
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineMetaHash,
|
'baseline_hash' => $baselineMetaHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
'display_name' => 'Policy Meta',
|
'display_name' => (string) $policy->display_name,
|
||||||
'evidence' => [
|
'evidence' => [
|
||||||
'fidelity' => 'meta',
|
'fidelity' => 'meta',
|
||||||
'source' => 'inventory',
|
'source' => 'inventory',
|
||||||
@ -123,7 +133,7 @@
|
|||||||
$context = is_array($run->context) ? $run->context : [];
|
$context = is_array($run->context) ? $run->context : [];
|
||||||
|
|
||||||
expect(data_get($context, 'baseline_compare.fidelity'))->toBe('meta');
|
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);
|
expect(data_get($context, 'baseline_compare.coverage.baseline_meta'))->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,18 +174,27 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$baselineContentHash = app(DriftHasher::class)->hashNormalized(
|
$baselineContentHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineContentHash,
|
'baseline_hash' => $baselineContentHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
'display_name' => 'Policy Content',
|
'display_name' => (string) $policy->display_name,
|
||||||
'evidence' => [
|
'evidence' => [
|
||||||
'fidelity' => 'content',
|
'fidelity' => 'content',
|
||||||
'source' => 'policy_version',
|
'source' => 'policy_version',
|
||||||
@ -234,7 +253,7 @@
|
|||||||
$context = is_array($run->context) ? $run->context : [];
|
$context = is_array($run->context) ? $run->context : [];
|
||||||
|
|
||||||
expect(data_get($context, 'baseline_compare.fidelity'))->toBe('meta');
|
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.coverage.baseline_content'))->toBe(1);
|
||||||
expect(data_get($context, 'baseline_compare.evidence_gaps.missing_current'))->toBe(1);
|
expect(data_get($context, 'baseline_compare.evidence_gaps.missing_current'))->toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||||
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
@ -51,14 +54,23 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
@ -169,10 +181,17 @@
|
|||||||
metaJsonb: $baselineMetaJsonb,
|
metaJsonb: $baselineMetaJsonb,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name);
|
||||||
|
expect($subjectKey)->not->toBeNull();
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||||
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
@ -51,18 +54,27 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$baselineHash = app(DriftHasher::class)->hashNormalized(
|
$baselineHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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,
|
'policy_type' => (string) $policy->policy_type,
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
'meta_jsonb' => [
|
'meta_jsonb' => [
|
||||||
'display_name' => 'Policy Provenance',
|
'display_name' => (string) $policy->display_name,
|
||||||
'evidence' => [
|
'evidence' => [
|
||||||
'fidelity' => 'content',
|
'fidelity' => 'content',
|
||||||
'source' => 'policy_version',
|
'source' => 'policy_version',
|
||||||
|
|||||||
@ -6,7 +6,9 @@
|
|||||||
use App\Services\Baselines\BaselineSnapshotIdentity;
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
||||||
use App\Services\Baselines\CurrentStateHashResolver;
|
use App\Services\Baselines\CurrentStateHashResolver;
|
||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
||||||
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
||||||
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
it('Baseline resolver prefers content evidence over meta evidence when available', function () {
|
it('Baseline resolver prefers content evidence over meta evidence when available', function () {
|
||||||
@ -46,13 +48,15 @@
|
|||||||
'last_seen_operation_run_id' => null,
|
'last_seen_operation_run_id' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$expectedContentHash = app(DriftHasher::class)->hashNormalized(
|
$expectedContentHash = app(DriftHasher::class)->hashNormalized([
|
||||||
app(SettingsNormalizer::class)->normalizeForDiff(
|
'settings' => app(SettingsNormalizer::class)->normalizeForDiff(
|
||||||
is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [],
|
is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [],
|
||||||
(string) $policyVersion->policy_type,
|
(string) $policyVersion->policy_type,
|
||||||
is_string($policyVersion->platform) ? $policyVersion->platform : null,
|
is_string($policyVersion->platform) ? $policyVersion->platform : null,
|
||||||
),
|
),
|
||||||
);
|
'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]),
|
||||||
|
'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]),
|
||||||
|
]);
|
||||||
|
|
||||||
$expectedMetaHash = app(BaselineSnapshotIdentity::class)->hashItemContent(
|
$expectedMetaHash = app(BaselineSnapshotIdentity::class)->hashItemContent(
|
||||||
policyType: (string) $inventory->policy_type,
|
policyType: (string) $inventory->policy_type,
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
use App\Services\Drift\DriftHasher;
|
use App\Services\Drift\DriftHasher;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunOutcome;
|
use App\Support\OperationRunOutcome;
|
||||||
use App\Support\OperationRunStatus;
|
use App\Support\OperationRunStatus;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
@ -43,13 +44,19 @@
|
|||||||
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE'],
|
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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'covered-uuid',
|
'subject_external_id' => $coveredWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $coveredSubjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => $hasher->hashNormalized($coveredContract),
|
'baseline_hash' => $hasher->hashNormalized($coveredContract),
|
||||||
'meta_jsonb' => ['display_name' => 'Covered Policy'],
|
'meta_jsonb' => ['display_name' => $coveredDisplayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$uncoveredContract = $builder->build(
|
$uncoveredContract = $builder->build(
|
||||||
@ -58,13 +65,19 @@
|
|||||||
metaJsonb: ['odata_type' => '#microsoft.graph.deviceCompliancePolicy', 'etag' => 'E_BASELINE'],
|
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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'uncovered-uuid',
|
'subject_external_id' => $uncoveredWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $uncoveredSubjectKey,
|
||||||
'policy_type' => 'deviceCompliancePolicy',
|
'policy_type' => 'deviceCompliancePolicy',
|
||||||
'baseline_hash' => $hasher->hashNormalized($uncoveredContract),
|
'baseline_hash' => $hasher->hashNormalized($uncoveredContract),
|
||||||
'meta_jsonb' => ['display_name' => 'Uncovered Policy'],
|
'meta_jsonb' => ['display_name' => $uncoveredDisplayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$inventorySyncRun = OperationRun::factory()->create([
|
$inventorySyncRun = OperationRun::factory()->create([
|
||||||
@ -93,7 +106,7 @@
|
|||||||
'external_id' => 'covered-uuid',
|
'external_id' => 'covered-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT'],
|
'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_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Settings\SettingsResolver;
|
use App\Services\Settings\SettingsResolver;
|
||||||
|
use App\Support\Baselines\BaselineSubjectKey;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use App\Support\OpsUx\OperationSummaryKeys;
|
use App\Support\OpsUx\OperationSummaryKeys;
|
||||||
|
|
||||||
@ -42,22 +43,46 @@
|
|||||||
statusByType: ['deviceConfiguration' => 'succeeded'],
|
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
|
// Baseline has policyA and policyB
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-a-uuid',
|
'subject_external_id' => $workspaceSafeExternalIdA,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'subject_key' => (string) $subjectKeyA,
|
||||||
'baseline_hash' => hash('sha256', 'content-a'),
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['display_name' => 'Policy A'],
|
'baseline_hash' => $baselineHashA,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayNameA],
|
||||||
]);
|
]);
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-b-uuid',
|
'subject_external_id' => $workspaceSafeExternalIdB,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'subject_key' => (string) $subjectKeyB,
|
||||||
'baseline_hash' => hash('sha256', 'content-b'),
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['display_name' => 'Policy B'],
|
'baseline_hash' => $baselineHashB,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayNameB],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Tenant has policyA (different content) and policyC (unexpected)
|
// Tenant has policyA (different content) and policyC (unexpected)
|
||||||
@ -65,9 +90,9 @@
|
|||||||
'tenant_id' => $tenant->getKey(),
|
'tenant_id' => $tenant->getKey(),
|
||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
'external_id' => 'policy-a-uuid',
|
'external_id' => 'policy-a-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['different_content' => true],
|
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_A'],
|
||||||
'display_name' => 'Policy A modified',
|
'display_name' => $displayNameA,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -75,9 +100,9 @@
|
|||||||
'tenant_id' => $tenant->getKey(),
|
'tenant_id' => $tenant->getKey(),
|
||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
'external_id' => 'policy-c-uuid',
|
'external_id' => 'policy-c-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['new_policy' => true],
|
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_C'],
|
||||||
'display_name' => 'Policy C unexpected',
|
'display_name' => 'Policy C',
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -157,30 +182,54 @@
|
|||||||
statusByType: ['deviceConfiguration' => 'succeeded'],
|
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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-a-uuid',
|
'subject_external_id' => $workspaceSafeExternalIdA,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'subject_key' => (string) $subjectKeyA,
|
||||||
'baseline_hash' => hash('sha256', 'content-a'),
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['display_name' => 'Policy A'],
|
'baseline_hash' => $baselineHashA,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayNameA],
|
||||||
]);
|
]);
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-b-uuid',
|
'subject_external_id' => $workspaceSafeExternalIdB,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'subject_key' => (string) $subjectKeyB,
|
||||||
'baseline_hash' => hash('sha256', 'content-b'),
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['display_name' => 'Policy B'],
|
'baseline_hash' => $baselineHashB,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayNameB],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InventoryItem::factory()->create([
|
InventoryItem::factory()->create([
|
||||||
'tenant_id' => $tenant->getKey(),
|
'tenant_id' => $tenant->getKey(),
|
||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
'external_id' => 'policy-a-uuid',
|
'external_id' => 'policy-a-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => $policyType,
|
||||||
'meta_jsonb' => ['different_content' => true],
|
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_A'],
|
||||||
'display_name' => 'Policy A modified',
|
'display_name' => $displayNameA,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'settings-catalog-policy-uuid',
|
'subject_external_id' => $workspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $subjectKey,
|
||||||
'policy_type' => 'settingsCatalogPolicy',
|
'policy_type' => 'settingsCatalogPolicy',
|
||||||
'baseline_hash' => hash('sha256', 'content-a'),
|
'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.
|
// Inventory item exists, but it was NOT observed in the latest sync run.
|
||||||
@ -281,7 +336,7 @@
|
|||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
'external_id' => 'settings-catalog-policy-uuid',
|
'external_id' => 'settings-catalog-policy-uuid',
|
||||||
'policy_type' => 'settingsCatalogPolicy',
|
'policy_type' => 'settingsCatalogPolicy',
|
||||||
'display_name' => 'Settings Catalog A',
|
'display_name' => $displayName,
|
||||||
'meta_jsonb' => ['etag' => 'abc'],
|
'meta_jsonb' => ['etag' => 'abc'],
|
||||||
'last_seen_operation_run_id' => (int) $olderInventoryRun->getKey(),
|
'last_seen_operation_run_id' => (int) $olderInventoryRun->getKey(),
|
||||||
'last_seen_at' => now()->subMinutes(5),
|
'last_seen_at' => now()->subMinutes(5),
|
||||||
@ -351,13 +406,19 @@
|
|||||||
metaJsonb: ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_BASELINE'],
|
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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-x-uuid',
|
'subject_external_id' => $workspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $subjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => $hasher->hashNormalized($baselineContract),
|
'baseline_hash' => $hasher->hashNormalized($baselineContract),
|
||||||
'meta_jsonb' => ['display_name' => 'Policy X'],
|
'meta_jsonb' => ['display_name' => $displayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InventoryItem::factory()->create([
|
InventoryItem::factory()->create([
|
||||||
@ -366,7 +427,7 @@
|
|||||||
'external_id' => 'policy-x-uuid',
|
'external_id' => 'policy-x-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT_1'],
|
'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_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -453,7 +514,7 @@
|
|||||||
expect((string) ($finding->evidence_jsonb['current_hash'] ?? ''))->not->toBe($currentHash1);
|
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');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
$profile = BaselineProfile::factory()->active()->create([
|
$profile = BaselineProfile::factory()->active()->create([
|
||||||
@ -476,6 +537,11 @@
|
|||||||
);
|
);
|
||||||
$baselineHash = $hasher->hashNormalized($baselineContract);
|
$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([
|
$snapshot1 = BaselineSnapshot::factory()->create([
|
||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
'baseline_profile_id' => $profile->getKey(),
|
'baseline_profile_id' => $profile->getKey(),
|
||||||
@ -484,9 +550,11 @@
|
|||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot1->getKey(),
|
'baseline_snapshot_id' => $snapshot1->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-x-uuid',
|
'subject_external_id' => $workspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $subjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InventoryItem::factory()->create([
|
InventoryItem::factory()->create([
|
||||||
@ -495,6 +563,7 @@
|
|||||||
'external_id' => 'policy-x-uuid',
|
'external_id' => 'policy-x-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT'],
|
'meta_jsonb' => ['odata_type' => '#microsoft.graph.deviceConfiguration', 'etag' => 'E_CURRENT'],
|
||||||
|
'display_name' => $displayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -520,13 +589,14 @@
|
|||||||
|
|
||||||
$scopeKey = 'baseline_profile:'.$profile->getKey();
|
$scopeKey = 'baseline_profile:'.$profile->getKey();
|
||||||
|
|
||||||
$fingerprint1 = (string) Finding::query()
|
$finding = Finding::query()
|
||||||
->where('tenant_id', $tenant->getKey())
|
->where('tenant_id', $tenant->getKey())
|
||||||
->where('source', 'baseline.compare')
|
->where('source', 'baseline.compare')
|
||||||
->where('scope_key', $scopeKey)
|
->where('scope_key', $scopeKey)
|
||||||
->orderBy('id')
|
->sole();
|
||||||
->firstOrFail()
|
|
||||||
->fingerprint;
|
expect($finding->times_seen)->toBe(1);
|
||||||
|
$fingerprint1 = (string) $finding->fingerprint;
|
||||||
|
|
||||||
$snapshot2 = BaselineSnapshot::factory()->create([
|
$snapshot2 = BaselineSnapshot::factory()->create([
|
||||||
'workspace_id' => $tenant->workspace_id,
|
'workspace_id' => $tenant->workspace_id,
|
||||||
@ -536,9 +606,11 @@
|
|||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot2->getKey(),
|
'baseline_snapshot_id' => $snapshot2->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'policy-x-uuid',
|
'subject_external_id' => $workspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $subjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => $baselineHash,
|
'baseline_hash' => $baselineHash,
|
||||||
|
'meta_jsonb' => ['display_name' => $displayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$run2 = $opService->ensureRunWithIdentity(
|
$run2 = $opService->ensureRunWithIdentity(
|
||||||
@ -566,9 +638,9 @@
|
|||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
expect($findings)->toHaveCount(2);
|
expect($findings)->toHaveCount(1);
|
||||||
expect($findings->pluck('fingerprint')->unique()->count())->toBe(2);
|
expect((string) $findings->first()?->fingerprint)->toBe($fingerprint1);
|
||||||
expect($findings->pluck('fingerprint')->all())->toContain($fingerprint1);
|
expect((int) $findings->first()?->times_seen)->toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates zero findings when baseline matches tenant inventory exactly', function () {
|
it('creates zero findings when baseline matches tenant inventory exactly', function () {
|
||||||
@ -607,13 +679,19 @@
|
|||||||
metaJsonb: $metaContent,
|
metaJsonb: $metaContent,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$displayName = 'Matching Policy';
|
||||||
|
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
|
||||||
|
expect($subjectKey)->not->toBeNull();
|
||||||
|
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'matching-uuid',
|
'subject_external_id' => $workspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $subjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => $contentHash,
|
'baseline_hash' => $contentHash,
|
||||||
'meta_jsonb' => ['display_name' => 'Matching Policy'],
|
'meta_jsonb' => ['display_name' => $displayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Tenant inventory with same content → same hash
|
// Tenant inventory with same content → same hash
|
||||||
@ -623,7 +701,7 @@
|
|||||||
'external_id' => 'matching-uuid',
|
'external_id' => 'matching-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => $metaContent,
|
'meta_jsonb' => $metaContent,
|
||||||
'display_name' => 'Matching Policy',
|
'display_name' => $displayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -698,22 +776,37 @@
|
|||||||
metaJsonb: $metaContent,
|
metaJsonb: $metaContent,
|
||||||
));
|
));
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
$displayName = 'Matching Policy';
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
$subjectKey = BaselineSubjectKey::fromDisplayName($displayName);
|
||||||
'subject_type' => 'policy',
|
expect($subjectKey)->not->toBeNull();
|
||||||
'subject_external_id' => 'matching-uuid',
|
$workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $subjectKey);
|
||||||
'policy_type' => 'deviceConfiguration',
|
|
||||||
'baseline_hash' => $contentHash,
|
|
||||||
'meta_jsonb' => ['display_name' => 'Matching Policy'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'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',
|
'policy_type' => 'notificationMessageTemplate',
|
||||||
'baseline_hash' => hash('sha256', 'foundation-content'),
|
'baseline_hash' => hash('sha256', 'foundation-content'),
|
||||||
'meta_jsonb' => ['display_name' => 'Foundation Template'],
|
'meta_jsonb' => ['display_name' => $foundationDisplayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InventoryItem::factory()->create([
|
InventoryItem::factory()->create([
|
||||||
@ -722,7 +815,7 @@
|
|||||||
'external_id' => 'matching-uuid',
|
'external_id' => 'matching-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => $metaContent,
|
'meta_jsonb' => $metaContent,
|
||||||
'display_name' => 'Matching Policy',
|
'display_name' => $displayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -733,7 +826,7 @@
|
|||||||
'external_id' => 'foundation-uuid',
|
'external_id' => 'foundation-uuid',
|
||||||
'policy_type' => 'notificationMessageTemplate',
|
'policy_type' => 'notificationMessageTemplate',
|
||||||
'meta_jsonb' => ['some' => 'value'],
|
'meta_jsonb' => ['some' => 'value'],
|
||||||
'display_name' => 'Foundation Template',
|
'display_name' => $foundationDisplayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -795,21 +888,34 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 2 baseline items: one will be missing (high), one will be different (medium)
|
// 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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'missing-uuid',
|
'subject_external_id' => $missingWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $missingSubjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => hash('sha256', 'missing-content'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'changed-uuid',
|
'subject_external_id' => $changedWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $changedSubjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => hash('sha256', 'original-content'),
|
'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)
|
// Tenant only has changed-uuid with different content + extra-uuid (unexpected)
|
||||||
@ -819,7 +925,7 @@
|
|||||||
'external_id' => 'changed-uuid',
|
'external_id' => 'changed-uuid',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => ['modified_content' => true],
|
'meta_jsonb' => ['modified_content' => true],
|
||||||
'display_name' => 'Changed Policy',
|
'display_name' => $changedDisplayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
@ -1126,21 +1232,34 @@
|
|||||||
statusByType: ['deviceConfiguration' => 'succeeded'],
|
statusByType: ['deviceConfiguration' => 'succeeded'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$missingDisplayName = 'Missing Policy';
|
||||||
|
$missingSubjectKey = BaselineSubjectKey::fromDisplayName($missingDisplayName);
|
||||||
|
expect($missingSubjectKey)->not->toBeNull();
|
||||||
|
$missingWorkspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId('deviceConfiguration', (string) $missingSubjectKey);
|
||||||
|
|
||||||
BaselineSnapshotItem::factory()->create([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'missing-policy',
|
'subject_external_id' => $missingWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $missingSubjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => hash('sha256', 'baseline-a'),
|
'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([
|
BaselineSnapshotItem::factory()->create([
|
||||||
'baseline_snapshot_id' => $snapshot->getKey(),
|
'baseline_snapshot_id' => $snapshot->getKey(),
|
||||||
'subject_type' => 'policy',
|
'subject_type' => 'policy',
|
||||||
'subject_external_id' => 'different-policy',
|
'subject_external_id' => $differentWorkspaceSafeExternalId,
|
||||||
|
'subject_key' => (string) $differentSubjectKey,
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'baseline_hash' => hash('sha256', 'baseline-b'),
|
'baseline_hash' => hash('sha256', 'baseline-b'),
|
||||||
'meta_jsonb' => ['display_name' => 'Different Policy'],
|
'meta_jsonb' => ['display_name' => $differentDisplayName],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InventoryItem::factory()->create([
|
InventoryItem::factory()->create([
|
||||||
@ -1149,7 +1268,7 @@
|
|||||||
'external_id' => 'different-policy',
|
'external_id' => 'different-policy',
|
||||||
'policy_type' => 'deviceConfiguration',
|
'policy_type' => 'deviceConfiguration',
|
||||||
'meta_jsonb' => ['different_content' => true],
|
'meta_jsonb' => ['different_content' => true],
|
||||||
'display_name' => 'Different Policy',
|
'display_name' => $differentDisplayName,
|
||||||
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user