Spec 423 security compliance readiness pack implementation. Head commit: c49acba7.
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #490
220 lines
7.3 KiB
PHP
220 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TenantConfiguration;
|
|
|
|
final class SecurityComplianceReadinessEvaluator
|
|
{
|
|
/**
|
|
* @param array<string, mixed>|null $normalizedPayload
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed>|null $compareResult
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function evaluate(
|
|
string $canonicalType,
|
|
?array $normalizedPayload,
|
|
array $context = [],
|
|
?array $compareResult = null,
|
|
): array {
|
|
if ($normalizedPayload === null) {
|
|
return $this->result(
|
|
state: 'readiness_not_assessed',
|
|
label: 'Not assessed',
|
|
reason: 'No structured Security and Compliance evidence is available for operator review.',
|
|
blockers: ['not_assessed'],
|
|
);
|
|
}
|
|
|
|
if (($normalizedPayload['supported'] ?? false) !== true) {
|
|
return $this->result(
|
|
state: 'readiness_blocked_unsupported',
|
|
label: 'Blocked',
|
|
reason: 'This Security and Compliance resource type is not supported by this readiness pack.',
|
|
blockers: ['unsupported_type'],
|
|
);
|
|
}
|
|
|
|
if ($this->evidenceBlocked($context)) {
|
|
return $this->result(
|
|
state: $this->permissionBlocked($context) ? 'readiness_blocked_permission' : 'readiness_blocked_evidence',
|
|
label: 'Blocked',
|
|
reason: $this->permissionBlocked($context)
|
|
? 'Provider permission or source availability blocks reliable operator review.'
|
|
: 'Content-backed captured evidence is required before readiness can be assessed.',
|
|
blockers: [$this->permissionBlocked($context) ? 'permission_blocked' : 'evidence_blocked'],
|
|
);
|
|
}
|
|
|
|
if ($this->identityBlocked($context)) {
|
|
return $this->result(
|
|
state: 'readiness_blocked_identity',
|
|
label: 'Blocked',
|
|
reason: 'Identity is unsafe for Security and Compliance readiness assessment.',
|
|
blockers: ['identity_blocked'],
|
|
);
|
|
}
|
|
|
|
$unsupportedFields = $this->actionableUnsupportedFields($normalizedPayload);
|
|
$manualReviewFields = $this->diagnosticFields($normalizedPayload, 'manual_review_fields');
|
|
|
|
if ($manualReviewFields !== []) {
|
|
return $this->result(
|
|
state: 'readiness_requires_manual_review',
|
|
label: 'Manual review required',
|
|
reason: 'Unsupported Security and Compliance fields require internal operator review.',
|
|
manualReviewRequired: true,
|
|
blockers: array_values(array_unique([
|
|
...array_map(static fn (string $field): string => 'manual_review:'.$field, $manualReviewFields),
|
|
])),
|
|
);
|
|
}
|
|
|
|
if ($unsupportedFields !== []) {
|
|
return $this->result(
|
|
state: 'readiness_blocked_unsupported',
|
|
label: 'Blocked',
|
|
reason: 'Unsupported fields require review before this evidence can be trusted.',
|
|
blockers: array_values(array_unique([
|
|
...array_map(static fn (string $field): string => 'unsupported:'.$field, $unsupportedFields),
|
|
])),
|
|
);
|
|
}
|
|
|
|
if ($this->requiresManualReview($compareResult)) {
|
|
return $this->result(
|
|
state: 'readiness_requires_manual_review',
|
|
label: 'Manual review required',
|
|
reason: 'Critical Security and Compliance changes require internal operator review.',
|
|
manualReviewRequired: true,
|
|
blockers: ['critical_material_change'],
|
|
);
|
|
}
|
|
|
|
return $this->result(
|
|
state: 'readiness_ready_for_operator_review',
|
|
label: 'Ready for operator review',
|
|
reason: 'Structured evidence is available for internal operator review.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function result(
|
|
string $state,
|
|
string $label,
|
|
string $reason,
|
|
bool $manualReviewRequired = false,
|
|
array $blockers = [],
|
|
): array {
|
|
return [
|
|
'state' => $state,
|
|
'label' => $label,
|
|
'reason' => $reason,
|
|
'manual_review_required' => $manualReviewRequired,
|
|
'blockers' => array_values($blockers),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function evidenceBlocked(array $context): bool
|
|
{
|
|
return $this->permissionBlocked($context)
|
|
|| ($context['evidence_state'] ?? null) !== 'content_backed'
|
|
|| ($context['capture_outcome'] ?? null) !== 'captured'
|
|
|| ! in_array($context['coverage_level'] ?? null, ['renderable', 'comparable'], true);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function permissionBlocked(array $context): bool
|
|
{
|
|
return (bool) ($context['permission_blocked'] ?? false);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function identityBlocked(array $context): bool
|
|
{
|
|
return in_array($context['identity_state'] ?? null, [
|
|
'identity_conflict',
|
|
'missing_external_id',
|
|
'unsupported_identity',
|
|
], true);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $compareResult
|
|
*/
|
|
private function requiresManualReview(?array $compareResult): bool
|
|
{
|
|
if ($compareResult === null) {
|
|
return false;
|
|
}
|
|
|
|
if (($compareResult['classification'] ?? null) === 'manual_review_required') {
|
|
return true;
|
|
}
|
|
|
|
$changes = $compareResult['changes'] ?? [];
|
|
|
|
if (! is_array($changes)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($changes as $change) {
|
|
if (! is_array($change)) {
|
|
continue;
|
|
}
|
|
|
|
if (($change['classification'] ?? null) === 'manual_review_required'
|
|
|| ($change['importance'] ?? null) === 'critical'
|
|
|| ($change['importance'] ?? null) === 'manual_review_required'
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return list<string>
|
|
*/
|
|
private function diagnosticFields(array $payload, string $key): array
|
|
{
|
|
$fields = data_get($payload, 'diagnostics.'.$key, []);
|
|
|
|
if (! is_array($fields)) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter(
|
|
array_map(static fn (mixed $field): string => is_string($field) ? trim($field) : '', $fields),
|
|
static fn (string $field): bool => $field !== '',
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return list<string>
|
|
*/
|
|
private function actionableUnsupportedFields(array $payload): array
|
|
{
|
|
$unsupportedFields = $this->diagnosticFields($payload, 'unsupported_fields');
|
|
$redactedFields = $this->diagnosticFields($payload, 'redacted_fields');
|
|
|
|
return array_values(array_filter(
|
|
$unsupportedFields,
|
|
static fn (string $field): bool => ! in_array($field, $redactedFields, true),
|
|
));
|
|
}
|
|
}
|