TenantAtlas/apps/platform/app/Services/TenantConfiguration/SecurityComplianceReadinessEvaluator.php
Ahmed Darrazi c49acba7cd
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m20s
feat: complete spec 423 security compliance readiness pack
2026-06-30 13:57:10 +02:00

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),
));
}
}