Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m7s
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.
123 lines
5.2 KiB
PHP
123 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Support\Baselines\Compare\CompareFindingCandidate;
|
|
use App\Support\Baselines\Compare\CompareState;
|
|
use App\Support\Baselines\Compare\CompareSubjectIdentity;
|
|
use App\Support\Baselines\Compare\CompareSubjectProjection;
|
|
use App\Support\Baselines\Compare\CompareSubjectResult;
|
|
|
|
it('serializes compare subject results with structured finding and diagnostics payloads', function (): void {
|
|
$result = new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity(
|
|
domainKey: 'entra',
|
|
subjectClass: 'control',
|
|
subjectTypeKey: 'conditionalAccessPolicy',
|
|
externalSubjectId: 'cap-1',
|
|
subjectKey: 'cap-1',
|
|
),
|
|
projection: new CompareSubjectProjection(
|
|
platformSubjectClass: 'control',
|
|
domainKey: 'entra',
|
|
subjectTypeKey: 'conditionalAccessPolicy',
|
|
operatorLabel: 'Conditional Access Policy',
|
|
summaryKind: 'control_snapshot',
|
|
additionalLabels: ['family' => 'identity'],
|
|
),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'available',
|
|
compareState: CompareState::Drift,
|
|
trustLevel: 'limited_confidence',
|
|
evidenceQuality: 'meta',
|
|
severityRecommendation: 'medium',
|
|
findingCandidate: new CompareFindingCandidate(
|
|
changeType: 'different_version',
|
|
severity: 'medium',
|
|
fingerprintBasis: [
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'subject_key' => 'cap-1',
|
|
'change_type' => 'different_version',
|
|
],
|
|
evidencePayload: [
|
|
'summary' => ['kind' => 'control_snapshot'],
|
|
],
|
|
),
|
|
diagnostics: [
|
|
'strategy_key' => 'future_control',
|
|
],
|
|
);
|
|
|
|
$payload = $result->toArray();
|
|
|
|
expect($result->hasFindingCandidate())->toBeTrue()
|
|
->and($result->isGapState())->toBeFalse()
|
|
->and($payload['compare_state'])->toBe(CompareState::Drift->value)
|
|
->and($payload['baseline_availability'])->toBe('available')
|
|
->and($payload['current_state_availability'])->toBe('available')
|
|
->and($payload['projection']['platform_subject_class'])->toBe('control')
|
|
->and($payload['projection']['summary_kind'])->toBe('control_snapshot')
|
|
->and($payload['finding_candidate']['change_type'])->toBe('different_version')
|
|
->and($payload['finding_candidate']['severity'])->toBe('medium')
|
|
->and($payload['diagnostics']['strategy_key'])->toBe('future_control');
|
|
});
|
|
|
|
it('requires a finding candidate for drift results', function (): void {
|
|
expect(fn (): CompareSubjectResult => new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('intune', 'policy', 'deviceConfiguration', 'policy-1', 'policy-1'),
|
|
projection: new CompareSubjectProjection('policy', 'intune', 'deviceConfiguration', 'Policy 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'missing',
|
|
compareState: CompareState::Drift,
|
|
trustLevel: 'limited_confidence',
|
|
evidenceQuality: 'meta',
|
|
))->toThrow(InvalidArgumentException::class, 'require a finding candidate');
|
|
});
|
|
|
|
it('rejects non-drift results that still try to write findings', function (): void {
|
|
expect(fn (): CompareSubjectResult => new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
|
|
projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'unknown',
|
|
compareState: CompareState::Incomplete,
|
|
trustLevel: 'unusable',
|
|
evidenceQuality: 'missing',
|
|
findingCandidate: new CompareFindingCandidate(
|
|
changeType: 'different_version',
|
|
severity: 'high',
|
|
fingerprintBasis: ['subject_key' => 'cap-1'],
|
|
evidencePayload: [],
|
|
),
|
|
))->toThrow(InvalidArgumentException::class, 'Only drift compare subject results');
|
|
});
|
|
|
|
it('treats unsupported, incomplete, ambiguous, and failed states as gap states', function (): void {
|
|
$states = [
|
|
CompareState::Unsupported,
|
|
CompareState::Incomplete,
|
|
CompareState::Ambiguous,
|
|
CompareState::Failed,
|
|
];
|
|
|
|
foreach ($states as $state) {
|
|
$result = new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
|
|
projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'unknown',
|
|
compareState: $state,
|
|
trustLevel: 'unusable',
|
|
evidenceQuality: 'missing',
|
|
diagnostics: [
|
|
'reason_code' => 'compare_failed',
|
|
'gap_record' => ['reason_code' => 'compare_failed'],
|
|
],
|
|
);
|
|
|
|
expect($result->isGapState())->toBeTrue()
|
|
->and($result->gapReasonCode())->toBe('compare_failed')
|
|
->and($result->gapRecord())->toBe(['reason_code' => 'compare_failed']);
|
|
}
|
|
});
|